diff --git a/.gitignore b/.gitignore index d40abb01..fe8b9c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ build/Release # Deployed apps should consider commenting this line out: # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git node_modules +package-lock.json .idea @@ -32,3 +33,4 @@ node_modules /local/super-request/.npmignore /local/supertest/.gitignore /package-lock.json +/scratch diff --git a/README.md b/README.md index 4942ac51..f8813220 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,18 @@ LRS Conformance Test Suite This is a NodeJS project that tests the 'MUST' requirements of the [xAPI Spec](https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-About.md#experience-api) and is based on the ADL [testing requirements](https://adl.gitbooks.io/xapi-lrs-conformance-requirements/content/) repository. This is actively being developed and new tests will be periodically added based on the testing requirements. Currently, this test suite only supports basic authentication. This test suite should also not run against a production LRS endpoint because the data is persisted and never voided. +### xAPI 2.0 Update + +xAPI 2.0 changed a number of requirements for the LRS from previous versions; most of these are additions supporting new features, while others have been removed. The details of the changes may be seen in the updated xAPI Spec. + +The following are the major changes that warranted the creation of additional tests in the conformance suite: +- Full support and validation for context agents and context groups +- 2.0.x is a valid set of xAPI versions +- Timestamps may be represented in the RFC 3339 format (changed from strict ISO 8601) +- If a timestamp is not formatted to UTC, the LRS will convert the timestamp instead of rejecting the statement +- Alternate Request Syntax is no longer supported and therefore not tested +- LRS responses now include `Last-Modified` headers + ### Installation Dependency @@ -40,6 +52,7 @@ $ node bin/console_runner.js --help -l, --authorization_path [string] Path to OAuth user authorization endpoint (relative to endpoint) -g, --grep [string] Only run tests that match the given pattern -b, --bail Abort the battery if one test fails + -x, --xapiVersion [string] 🌟 New: Version of the xAPI spec to test against -d, --directory [value] Specific directories of tests (as a comma seperated list with no spaces) -z, --errors Results log of failing tests only ``` diff --git a/batteries.js b/batteries.js new file mode 100644 index 00000000..b69f75de --- /dev/null +++ b/batteries.js @@ -0,0 +1,13795 @@ +module.exports = { + "1.0.3": { + "conformanceTestCount": 1365, + "tests": { + "text": "", + "children": [ + { + "text": "Formatting Requirements", + "children": [ + { + "text": "A Statement contains an \"actor\" property", + "children": [ + { + "text": "statement \"actor\" missing", + "children": [] + } + ] + }, + { + "text": "A Statement contains a \"verb\" property", + "children": [ + { + "text": "statement \"verb\" missing", + "children": [] + } + ] + }, + { + "text": "A Statement contains an \"object\" property", + "children": [ + { + "text": "statement \"object\" missing", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request any Statement having a property whose value is set to \"null\", except in an \"extensions\" property", + "children": [ + { + "text": "statement actor should fail on \"null\"", + "children": [] + }, + { + "text": "statement verb should fail on \"null\"", + "children": [] + }, + { + "text": "statement context should fail on \"null\"", + "children": [] + }, + { + "text": "statement object should fail on \"null\"", + "children": [] + }, + { + "text": "statement activity extensions can be empty", + "children": [] + }, + { + "text": "statement result extensions can be empty", + "children": [] + }, + { + "text": "statement context extensions can be empty", + "children": [] + }, + { + "text": "statement substatement activity extensions can be empty", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data type", + "children": [ + { + "text": "with strings where numbers are required", + "children": [] + }, + { + "text": "even if those strings contain numbers", + "children": [] + }, + { + "text": "with strings where booleans are required", + "children": [] + }, + { + "text": "even if those strings contain booleans", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement which uses any non-format-following key or value, including the empty string, where a string with a particular format, such as mailto IRI, UUID, or IRI, is required.", + "children": [ + { + "text": "statement \"id\" invalid numeric", + "children": [] + }, + { + "text": "statement \"id\" invalid object", + "children": [] + }, + { + "text": "statement \"id\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement \"id\" invalid UUID with non A-F", + "children": [] + }, + { + "text": "statement actor \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement actor \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement authority \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement authority \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement context instructor \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement context instructor \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement context team \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement as \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement as \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group\" account \"name\" property is string", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement where the case of a key does not match the case specified in this specification.", + "children": [ + { + "text": "should fail when not using \"id\"", + "children": [] + }, + { + "text": "should fail when not using \"actor\"", + "children": [] + }, + { + "text": "should fail when not using \"verb\"", + "children": [] + }, + { + "text": "should fail when not using \"object\"", + "children": [] + }, + { + "text": "should fail when not using \"result\"", + "children": [] + }, + { + "text": "should fail when not using \"context\"", + "children": [] + }, + { + "text": "should fail when not using \"timestamp\"", + "children": [] + }, + { + "text": "should fail when not using \"stored\"", + "children": [] + }, + { + "text": "should fail when not using \"authority\"", + "children": [] + }, + { + "text": "should fail when not using \"version\"", + "children": [] + }, + { + "text": "should fail when not using \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement where the case of a value restricted to enumerated values does not match an enumerated value given in this specification exactly.", + "children": [ + { + "text": "when interactionType is wrong case (\"true-faLse\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"choiCe\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"fill-iN\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"long-fiLl-in\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"matchIng\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"perfOrmance\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"seqUencing\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"liKert\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"nUmeric\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"Other\")", + "children": [] + } + ] + }, + { + "text": "The LRS rejects with error code 400 Bad Request a token with does not validate as matching the RFC 5646 standard in the sequence of token lengths for language map keys.", + "children": [ + { + "text": "statement verb \"display\" should pass given de two letter language code only", + "children": [] + }, + { + "text": "statement verb \"display\" should fail given invalid language code", + "children": [] + }, + { + "text": "statement object \"name\" should pass given de-DE language-region code", + "children": [] + }, + { + "text": "statement object \"name\" should fail given invalid language code", + "children": [] + }, + { + "text": "statement object \"description\" should pass given zh-Hant language-script code", + "children": [] + }, + { + "text": "statement object \"description\" should fail given invalid language code", + "children": [] + }, + { + "text": "interaction components' description should pass given sr-Latn-RS language-script-region code", + "children": [] + }, + { + "text": "context.language should pass given three letter cmn language code", + "children": [] + }, + { + "text": "context.language should fail given invalid language code", + "children": [] + }, + { + "text": "statement attachment \"display\" should pass given es two letter language code only", + "children": [] + }, + { + "text": "statement attachment \"display\" should fail given invalid language code", + "children": [] + }, + { + "text": "statement attachment \"description\" should pass given es-MX language-UN region code", + "children": [] + }, + { + "text": "statement attachment \"description\" should fail given es-MX invalid language code", + "children": [] + }, + { + "text": "statement substatement verb \"display\" should pass given sr-Cyrl language-script code", + "children": [] + }, + { + "text": "statement substatement verb \"display\" should fail given invalid language code", + "children": [] + }, + { + "text": "statement substatement activity \"name\" should pass given zh-Hans-CN language-script-region code", + "children": [] + }, + { + "text": "statement substatement activity \"name\" should fail given zh-z-aaa-z-bbb-c-ccc invalid (two extensions with same single-letter prefix) language code", + "children": [] + }, + { + "text": "statement substatement activity \"description\" should pass given ase three letter language code", + "children": [] + }, + { + "text": "statement substatement activity \"description\" should fail given invalid language code", + "children": [] + }, + { + "text": "substatement interaction components' description should pass given ja two letter language code only", + "children": [] + }, + { + "text": "substatement context.language should pass given two letter fr-CA language-region code", + "children": [] + }, + { + "text": "substatement context.language should fail given invalid language code", + "children": [] + } + ] + }, + { + "text": "An LRS stores 32-bit floating point numbers with at least the precision of IEEE 754", + "children": [ + { + "text": "should pass and keep precision", + "children": [] + } + ] + }, + { + "text": "The LRS rejects with error code 400 Bad Request parameter values which do not validate to the same standards required for values of the same types in Statements", + "children": [ + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + } + ] + }, + { + "text": "All Objects are well-created JSON Objects", + "children": [ + { + "text": "An LRS rejects a not well-created JSON Object", + "children": [] + }, + { + "text": "Statements Verify Templates", + "children": [ + { + "text": "should pass statement template", + "children": [] + } + ] + }, + { + "text": "Agents Verify Templates", + "children": [ + { + "text": "should pass statement actor template", + "children": [] + }, + { + "text": "should pass statement authority template", + "children": [] + }, + { + "text": "should pass statement context instructor template", + "children": [] + }, + { + "text": "should pass statement substatement as agent template", + "children": [] + }, + { + "text": "should pass statement substatement\"s agent template", + "children": [] + }, + { + "text": "should pass statement substatement\"s context instructor template", + "children": [] + } + ] + }, + { + "text": "Groups Verify Templates", + "children": [ + { + "text": "should pass statement actor template", + "children": [] + }, + { + "text": "should pass statement authority template", + "children": [] + }, + { + "text": "should pass statement context instructor template", + "children": [] + }, + { + "text": "should pass statement context team template", + "children": [] + }, + { + "text": "should pass statement substatement as group template", + "children": [] + }, + { + "text": "should pass statement substatement\"s group template", + "children": [] + }, + { + "text": "should pass statement substatement\"s context instructor template", + "children": [] + }, + { + "text": "should pass statement substatement\"s context team template", + "children": [] + } + ] + }, + { + "text": "A Group is defined by \"objectType\" of an \"actor\" property or \"object\" property with value \"Group\"", + "children": [ + { + "text": "statement actor \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement authority \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement context team \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement substatement as group \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s group \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"objectType\" accepts \"Group\"", + "children": [] + } + ] + }, + { + "text": "An Anonymous Group is defined by \"objectType\" of an \"actor\" or \"object\" with value \"Group\" and by none of \"mbox\", \"mbox_sha1sum\", \"openid\", or \"account\" being used", + "children": [ + { + "text": "statement actor does not require functional identifier", + "children": [] + }, + { + "text": "statement authority does not require functional identifier", + "children": [] + }, + { + "text": "statement context instructor does not require functional identifier", + "children": [] + }, + { + "text": "statement context team does not require functional identifier", + "children": [] + }, + { + "text": "statement substatement as group does not require functional identifier", + "children": [] + }, + { + "text": "statement substatement\"s group does not require functional identifier", + "children": [] + }, + { + "text": "statement substatement\"s context instructor does not require functional identifier", + "children": [] + }, + { + "text": "statement substatement\"s context team does not require functional identifier", + "children": [] + } + ] + }, + { + "text": "Verbs Verify Templates", + "children": [ + { + "text": "should pass statement verb template", + "children": [] + }, + { + "text": "should pass substatement verb template", + "children": [] + } + ] + }, + { + "text": "Objects Verify Templates", + "children": [ + { + "text": "should pass statement activity template", + "children": [] + }, + { + "text": "should pass statement substatement activity template", + "children": [] + }, + { + "text": "should pass statement agent template", + "children": [] + }, + { + "text": "should pass statement substatement agent template", + "children": [] + }, + { + "text": "should pass statement group template", + "children": [] + }, + { + "text": "should pass statement substatement group template", + "children": [] + }, + { + "text": "should pass statement StatementRef template", + "children": [] + }, + { + "text": "should pass statement substatement StatementRef template", + "children": [] + }, + { + "text": "should pass statement SubStatement template", + "children": [] + } + ] + }, + { + "text": "Activities Verify Templates", + "children": [ + { + "text": "should pass statement activity default template", + "children": [] + }, + { + "text": "should pass statement substatement activity default template", + "children": [] + }, + { + "text": "should pass statement activity choice template", + "children": [] + }, + { + "text": "should pass statement activity fill-in template", + "children": [] + }, + { + "text": "should pass statement activity numeric template", + "children": [] + }, + { + "text": "should pass statement activity likert template", + "children": [] + }, + { + "text": "should pass statement activity long-fill-in template", + "children": [] + }, + { + "text": "should pass statement activity matching template", + "children": [] + }, + { + "text": "should pass statement activity other template", + "children": [] + }, + { + "text": "should pass statement activity performance template", + "children": [] + }, + { + "text": "should pass statement activity sequencing template", + "children": [] + }, + { + "text": "should pass statement activity true-false template", + "children": [] + }, + { + "text": "should pass statement substatement activity choice template", + "children": [] + }, + { + "text": "should pass statement substatement activity likert template", + "children": [] + }, + { + "text": "should pass statement substatement activity matching template", + "children": [] + }, + { + "text": "should pass statement substatement activity performance template", + "children": [] + }, + { + "text": "should pass statement substatement activity sequencing template", + "children": [] + } + ] + }, + { + "text": "An Activity Definition uses the following properties: name, description, type, moreInfo, interactionType, or extensions", + "children": [ + { + "text": "statement activity \"definition\" missing all properties", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"name\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"description\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"type\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"moreInfo\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"extensions\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"interactionType\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" missing all properties", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"name\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"description\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"type\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"moreInfo\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"extensions\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"interactionType\"", + "children": [] + } + ] + }, + { + "text": "SubStatements Verify Templates", + "children": [ + { + "text": "should pass statement SubStatement template", + "children": [] + } + ] + }, + { + "text": "StatementRefs Verify Templates", + "children": [ + { + "text": "should pass statement StatementRef template", + "children": [] + }, + { + "text": "should pass substatement StatementRef template", + "children": [] + } + ] + }, + { + "text": "Results Verify Templates", + "children": [ + { + "text": "should pass statement result template", + "children": [] + }, + { + "text": "should pass substatement result template", + "children": [] + } + ] + }, + { + "text": "Contexts Verify Templates", + "children": [ + { + "text": "should pass statement context template", + "children": [] + }, + { + "text": "should pass substatement context template", + "children": [] + } + ] + }, + { + "text": "A ContextActivity is defined as a single Activity of the \"value\" of the \"contextActivities\" property", + "children": [ + { + "text": "statement context \"contextActivities parent\" value is activity", + "children": [] + }, + { + "text": "statement context \"contextActivities grouping\" value is activity", + "children": [] + }, + { + "text": "statement context \"contextActivities category\" value is activity", + "children": [] + }, + { + "text": "statement context \"contextActivities other\" value is activity", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities parent\" value is activity", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities grouping\" value is activity", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities category\" value is activity", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities other\" value is activity", + "children": [] + } + ] + }, + { + "text": "Languages Verify Templates", + "children": [ + { + "text": "should pass statement verb template", + "children": [] + }, + { + "text": "should pass statement object template", + "children": [] + }, + { + "text": "should pass statement attachment template", + "children": [] + }, + { + "text": "should pass statement substatement verb template", + "children": [] + }, + { + "text": "should pass statement substatement object template", + "children": [] + } + ] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI values without a scheme.", + "children": [ + { + "text": "should fail with bad verb id scheme", + "children": [] + }, + { + "text": "should fail with bad verb openid scheme", + "children": [] + }, + { + "text": "should fail with bad account homePage", + "children": [] + }, + { + "text": "should fail with bad object id", + "children": [] + }, + { + "text": "should fail with bad object type", + "children": [] + }, + { + "text": "should fail with bad object moreInfo", + "children": [] + }, + { + "text": "should fail with attachment bad usageType", + "children": [] + }, + { + "text": "should fail with bad attachment fileUrl", + "children": [] + }, + { + "text": "should fail with bad object definition extension", + "children": [] + }, + { + "text": "should fail with bad context extension", + "children": [] + }, + { + "text": "should fail with bad result extension", + "children": [] + } + ] + } + ] + }, + { + "text": "Statement Lifecycle Requirements", + "children": [ + { + "text": "A Voiding Statement is defined as a Statement whose \"verb\" property's \"id\" property's IRI ending with \"voided\"", + "children": [ + { + "text": "statement verb voided IRI ends with \"voided\" (WARNING: this applies \"Upon receiving a Statement that voids another, the LRS SHOULD NOT* reject the request on the grounds of the Object of that voiding Statement not being present\")", + "children": [] + } + ] + }, + { + "text": "A Voiding Statement's \"objectType\" field has a value of \"StatementRef\"", + "children": [ + { + "text": "statement verb voided uses substatement with \"StatementRef\"", + "children": [] + }, + { + "text": "statement verb voided does not use object \"StatementRef\"", + "children": [] + } + ] + }, + { + "text": "A Voided Statement is defined as a Statement that is not a Voiding Statement and is the Target of a Voiding Statement within the LRS", + "children": [ + { + "text": "should return a voided statement when using GET \"voidedStatementId\"", + "children": [] + }, + { + "text": "should return 404 when using GET with \"statementId\"", + "children": [] + } + ] + }, + { + "text": "A Voiding Statement cannot Target another Voiding Statement", + "children": [ + { + "text": "should not void an already voided statement", + "children": [] + }, + { + "text": "should not void a voiding statement", + "children": [] + } + ] + } + ] + }, + { + "text": "Id Property Requirements", + "children": [ + { + "text": "All UUID types follow requirements of RFC4122", + "children": [ + { + "text": "statement \"id\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement \"id\" invalid UUID with non A-F", + "children": [] + }, + { + "text": "statement object statementref \"id\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement object statementref \"id\" invalid UUID with non A-F", + "children": [] + }, + { + "text": "statement context \"registration\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement context \"registration\" invalid UUID with non A-F", + "children": [] + }, + { + "text": "statement context \"statement\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement substatement context \"statement\" invalid UUID with non A-F", + "children": [] + } + ] + }, + { + "text": "All UUID types are in standard String form", + "children": [ + { + "text": "statement \"id\" invalid numeric", + "children": [] + }, + { + "text": "statement \"id\" invalid object", + "children": [] + }, + { + "text": "statement object statementref \"id\" invalid numeric", + "children": [] + }, + { + "text": "statement object statementref \"id\" invalid object", + "children": [] + }, + { + "text": "statement context \"registration\" invalid numeric", + "children": [] + }, + { + "text": "statement context \"registration\" invalid object", + "children": [] + }, + { + "text": "statement context \"statement\" invalid numeric", + "children": [] + }, + { + "text": "statement substatement context \"statement\" invalid object", + "children": [] + } + ] + }, + { + "text": "An LRS generates the \"id\" property of a Statement if none is provided", + "children": [ + { + "text": "should complete an empty id property", + "children": [] + } + ] + } + ] + }, + { + "text": "Actor Property Requirements", + "children": [ + { + "text": "An \"actor\" property's \"objectType\" property is either \"Agent\" or \"Group\"", + "children": [ + { + "text": "statement actor \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement actor \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement authority \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement authority \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement substatement as agent with \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement substatement as group with \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s actor \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement substatement\"s actor \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" should fail when not \"Group\"", + "children": [] + } + ] + }, + { + "text": "An \"objectType\" property is a String", + "children": [ + { + "text": "statement actor \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement actor \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement authority \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement authority \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement substatement as agent with \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement as agent with \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement substatement\"s agent \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement\"s agent \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" should fail object", + "children": [] + } + ] + }, + { + "text": "A \"name\" property is a String", + "children": [ + { + "text": "statement actor \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement actor \"name\" should fail object", + "children": [] + }, + { + "text": "statement authority \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement authority \"name\" should fail object", + "children": [] + }, + { + "text": "statement context instructor \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement context instructor \"name\" should fail object", + "children": [] + }, + { + "text": "statement substatement as agent with \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement as agent with \"name\" should fail object", + "children": [] + }, + { + "text": "statement substatement\"s agent \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement\"s agent \"name\" should fail object", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"name\" should fail object", + "children": [] + } + ] + }, + { + "text": "An \"actor\" property with \"objectType\" as \"Agent\" uses one of the following properties: \"mbox\", \"mbox_sha1sum\", \"openid\", \"account\"", + "children": [ + { + "text": "statement actor without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement actor \"account\" should pass", + "children": [] + }, + { + "text": "statement actor \"mbox\" should pass", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement actor \"openid\" should pass", + "children": [] + }, + { + "text": "statement authority without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement authority \"account\" should pass", + "children": [] + }, + { + "text": "statement authority \"mbox\" should pass", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement authority \"openid\" should pass", + "children": [] + }, + { + "text": "statement context instructor without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement context instructor \"account\" should pass", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" should pass", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement context instructor \"openid\" should pass", + "children": [] + }, + { + "text": "statement substatement as agent without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement substatement as agent \"account\" should pass", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox\" should pass", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement substatement as agent \"openid\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s agent without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s context instructor without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" should pass", + "children": [] + } + ] + }, + { + "text": "An Agent is defined by \"objectType\" of an \"actor\" property or \"object\" property with value \"Agent\"", + "children": [ + { + "text": "statement actor does not require objectType", + "children": [] + }, + { + "text": "statement actor \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement authority \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement substatement as agent \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" accepts \"Agent\"", + "children": [] + } + ] + }, + { + "text": "An Agent does not use the \"mbox\" property if \"mbox_sha1sum\", \"openid\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Agent does not use the \"mbox_sha1sum\" property if \"mbox\", \"openid\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Agent does not use the \"account\" property if \"mbox\", \"mbox_sha1sum\", or \"openid\" are used", + "children": [ + { + "text": "statement actor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as agent \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as agent \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as agent \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Agent does not use the \"openid\" property if \"mbox\", \"mbox_sha1sum\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as agent \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as agent \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as agent \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + } + ] + }, + { + "text": "An Anonymous Group uses the \"member\" property", + "children": [ + { + "text": "statement actor anonymous group missing member", + "children": [] + }, + { + "text": "statement authority anonymous group missing member", + "children": [] + }, + { + "text": "statement context instructor anonymous group missing member", + "children": [] + }, + { + "text": "statement context team anonymous group missing member", + "children": [] + }, + { + "text": "statement substatement as group anonymous group missing member", + "children": [] + }, + { + "text": "statement substatement\"s group anonymous group missing member", + "children": [] + }, + { + "text": "statement substatement\"s context instructor anonymous group missing member", + "children": [] + }, + { + "text": "statement substatement\"s context team anonymous group missing member", + "children": [] + } + ] + }, + { + "text": "The \"member\" property is an array of Objects following Agent requirements", + "children": [ + { + "text": "statement actor requires member type \"array\"", + "children": [] + }, + { + "text": "statement authority requires member type \"array\"", + "children": [] + }, + { + "text": "statement context instructor requires member type \"array\"", + "children": [] + }, + { + "text": "statement context team requires member type \"array\"", + "children": [] + }, + { + "text": "statement substatement as group requires member type \"array\"", + "children": [] + }, + { + "text": "statement substatement\"s group requires member type \"array\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor requires member type \"array\"", + "children": [] + }, + { + "text": "statement substatement\"s context team requires member type \"array\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group is defined by \"objectType\" of an \"actor\" or \"object\" with value \"Group\" and by one of \"mbox\", \"mbox_sha1sum\", \"openid\", or \"account\" being used", + "children": [ + { + "text": "statement actor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"account\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group uses one of the following properties: \"mbox\", \"mbox_sha1sum\", \"openid\", \"account\"", + "children": [ + { + "text": "statement actor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"account\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group does not use the \"mbox\" property if \"mbox_sha1sum\", \"openid\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor \"mbox\" cannot be used with \"openid", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context team \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context team \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group does not use the \"mbox_sha1sum\" property if \"mbox\", \"openid\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"openid", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context team \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context team \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context team \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group does not use the \"openid\" property if \"mbox\", \"mbox_sha1sum\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"openid\" cannot be used with \"account", + "children": [] + }, + { + "text": "statement actor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context team \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context team \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as group \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group does not use the \"account\" property if \"mbox\", \"mbox_sha1sum\", or \"openid\" are used", + "children": [ + { + "text": "statement actor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor \"account\" cannot be used with \"openid", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context team \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context team \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"account\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An \"mbox\" property has the form \"mailto:email address\" and is an IRI", + "children": [ + { + "text": "statement actor \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement actor \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement authority \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement authority \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement context instructor \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement context instructor \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement context team \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement as \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement as \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement actor \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement actor \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement authority \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement authority \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement context instructor \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement context instructor \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement context team \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement as \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement as \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group mbox\" not mailto:email address", + "children": [] + } + ] + }, + { + "text": "An \"mbox_sha1sum\" property is a String", + "children": [ + { + "text": "statement actor \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement actor \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement authority \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement authority \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement context instructor \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement context instructor \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement context team \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement as \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement as \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group mbox_sha1sum\" not string", + "children": [] + } + ] + }, + { + "text": "An \"openid\" property is a URI", + "children": [ + { + "text": "statement actor \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement actor \"group openid\" not URI", + "children": [] + }, + { + "text": "statement authority \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement authority \"group openid\" not URI", + "children": [] + }, + { + "text": "statement context instructor \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement context instructor \"group openid\" not URI", + "children": [] + }, + { + "text": "statement context team \"group openid\" not URI", + "children": [] + }, + { + "text": "statement substatement as \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement substatement as \"group openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s \"group openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group openid\" not URI", + "children": [] + } + ] + }, + { + "text": "An Account Object is the \"account\" property of a Group or Agent", + "children": [ + { + "text": "statement actor \"agent account\" property exists", + "children": [] + }, + { + "text": "statement actor \"group account\" property exists", + "children": [] + }, + { + "text": "statement authority \"agent account\" property exists", + "children": [] + }, + { + "text": "statement authority \"group account\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"agent account\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"group account\" property exists", + "children": [] + }, + { + "text": "statement context team \"group account\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"agent account\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"group account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"agent account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"group account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group account\" property exists", + "children": [] + } + ] + }, + { + "text": "An Account Object uses the \"homePage\" property", + "children": [ + { + "text": "statement actor \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement actor \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement authority \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement authority \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement context team \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group\" account \"homePage\" property exists", + "children": [] + } + ] + }, + { + "text": "An Account Object's \"homePage\" property is an IRL", + "children": [ + { + "text": "statement actor \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement actor \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement authority \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement authority \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement context instructor \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement context instructor \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement context team \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement as \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement as \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group\" account \"homePage property is IRL", + "children": [] + } + ] + }, + { + "text": "An Account Object uses the \"name\" property", + "children": [ + { + "text": "statement actor \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement actor \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement authority \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement authority \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement context team \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group\" account \"name\" property exists", + "children": [] + } + ] + } + ] + }, + { + "text": "Verb Property Requirements", + "children": [ + { + "text": "A \"verb\" property contains an \"id\" property", + "children": [ + { + "text": "statement verb missing \"id\"", + "children": [] + }, + { + "text": "statement substatement verb missing \"id\"", + "children": [] + } + ] + }, + { + "text": "A \"verb\" property's \"id\" property is an IRI", + "children": [ + { + "text": "statement verb \"id\" not IRI", + "children": [] + }, + { + "text": "statement substatement verb \"id\" not IRI", + "children": [] + } + ] + }, + { + "text": "A \"verb\" property's \"display\" property is a Language Map", + "children": [ + { + "text": "statement verb \"display\" is numeric", + "children": [] + }, + { + "text": "statement verb \"display\" is string", + "children": [] + }, + { + "text": "statement substatement verb \"display\" is numeric", + "children": [] + }, + { + "text": "statement substatement verb \"display\" is string", + "children": [] + } + ] + } + ] + }, + { + "text": "Object Property Requirements", + "children": [ + { + "text": "An \"object\" property's \"objectType\" property is either \"Activity\", \"Agent\", \"Group\", \"SubStatement\", or \"StatementRef\"", + "children": [ + { + "text": "statement activity should fail on \"activity\"", + "children": [] + }, + { + "text": "statement substatement activity should fail on \"activity\"", + "children": [] + }, + { + "text": "statement agent template should fail on \"agent\"", + "children": [] + }, + { + "text": "statement substatement agent should fail on \"agent\"", + "children": [] + }, + { + "text": "statement group should fail on \"group\"", + "children": [] + }, + { + "text": "statement substatement group should fail on \"group\"", + "children": [] + }, + { + "text": "statement StatementRef should fail on \"statementref\"", + "children": [] + }, + { + "text": "statement substatement StatementRef should fail on \"statementref\"", + "children": [] + }, + { + "text": "statement SubStatement should fail on \"substatement\"", + "children": [] + } + ] + }, + { + "text": "An \"object\" property uses the \"id\" property exactly one time", + "children": [ + { + "text": "statement activity \"id\" not provided", + "children": [] + }, + { + "text": "statement substatement activity \"id\" not provided", + "children": [] + } + ] + }, + { + "text": "An \"object\" property's \"id\" property is an IRI", + "children": [ + { + "text": "statement activity \"id\" not IRI", + "children": [] + }, + { + "text": "statement activity \"id\" is IRI", + "children": [] + }, + { + "text": "statement substatement activity \"id\" not IRI", + "children": [] + }, + { + "text": "statement substatement activity \"id\" is IRI", + "children": [] + } + ] + }, + { + "text": "An Activity's \"definition\" property is an Object", + "children": [ + { + "text": "statement activity \"definition\" not object", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" not object", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"name\" property is a Language Map", + "children": [ + { + "text": "statement object \"name\" language map is numeric", + "children": [] + }, + { + "text": "statement object \"name\" language map is string", + "children": [] + }, + { + "text": "statement substatement activity \"name\" language map is numeric", + "children": [] + }, + { + "text": "statement substatement activity \"name\" language map is string", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"description\" property is a Language Map", + "children": [ + { + "text": "statement object \"description\" language map is numeric", + "children": [] + }, + { + "text": "statement object \"description\" language map is string", + "children": [] + }, + { + "text": "statement substatement activity \"description\" language map is numeric", + "children": [] + }, + { + "text": "statement substatement activity \"description\" language map is string", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"type\" property is an IRI", + "children": [ + { + "text": "statement activity \"type\" not IRI", + "children": [] + }, + { + "text": "statement substatement activity \"type\" not IRI", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"moreinfo\" property is an IRL", + "children": [ + { + "text": "statement activity \"moreInfo\" not IRI", + "children": [] + }, + { + "text": "statement substatement activity \"moreInfo\" not IRI", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"interactionType\" property is a String with a value of either “true-false”, “choice”, “fill-in”, “long-fill-in”, “matching”, “performance”, “sequencing”, “likert”, “numeric” or “other”", + "children": [ + { + "text": "statement activity \"interactionType\" can be used with \"true-false\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"choice\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"fill-in\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"long-fill-in\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"matching\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"performance\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"sequencing\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"likert\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"numeric\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"other\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" fails with invalid iri", + "children": [] + }, + { + "text": "statement activity \"interactionType\" fails with invalid numeric", + "children": [] + }, + { + "text": "statement activity \"interactionType\" fails with invalid object", + "children": [] + }, + { + "text": "statement activity \"interactionType\" fails with invalid string", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"true-false\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"choice\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"fill-in\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"long-fill-in\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"matching\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"performance\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"sequencing\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"likert\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"numeric\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"other\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" fails with invalid iri", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" fails with invalid numeric", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" fails with invalid object", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" fails with invalid string", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"extension\" property is an Object", + "children": [ + { + "text": "statement activity \"extension\" invalid string", + "children": [] + }, + { + "text": "statement activity \"extension\" invalid iri", + "children": [] + }, + { + "text": "statement substatement activity \"extension\" invalid string", + "children": [] + }, + { + "text": "statement substatement activity \"extension\" invalid iri", + "children": [] + } + ] + }, + { + "text": "An Activity Definition uses the \"interactionType\" property if any of the correctResponsesPattern, choices, scale, source, target, or steps properties are used", + "children": [ + { + "text": "Activity Definition uses correctResponsesPattern without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses choices without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses fill-in without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses scale without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses long-fill-in without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses source without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses target without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses numeric without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses other without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses performance without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses sequencing without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses true-false without \"interactionType\" property", + "children": [] + } + ] + }, + { + "text": "Statements that use an Agent or Group as an Object MUST specify an \"objectType\" property.", + "children": [ + { + "text": "should fail when using agent as object and no objectType", + "children": [] + }, + { + "text": "should fail when using group as object and no objectType", + "children": [] + }, + { + "text": "substatement should fail when using agent as object and no objectType", + "children": [] + }, + { + "text": "substatement should fail when using group as object and no objectType", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement is defined by the \"objectType\" of an \"object\" with value \"SubStatement\"", + "children": [ + { + "text": "substatement invalid when not \"SubStatement\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement follows the requirements of all Statements", + "children": [ + { + "text": "substatement requires actor", + "children": [] + }, + { + "text": "substatement requires object", + "children": [] + }, + { + "text": "substatement requires verb", + "children": [] + }, + { + "text": "should pass substatement context", + "children": [] + }, + { + "text": "should pass substatement result", + "children": [] + }, + { + "text": "should pass substatement statementref", + "children": [] + }, + { + "text": "should pass substatement as agent", + "children": [] + }, + { + "text": "should pass substatement as group", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot have a Sub-Statement", + "children": [ + { + "text": "substatement invalid nested \"SubStatement\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot use the \"id\" property at the Statement level", + "children": [ + { + "text": "substatement invalid with property \"id\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot use the \"stored\" property", + "children": [ + { + "text": "substatement invalid with property \"stored\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot use the \"version\" property", + "children": [ + { + "text": "substatement invalid with property \"version\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot use the \"authority\" property", + "children": [ + { + "text": "substatement invalid with property \"authority\"", + "children": [] + } + ] + }, + { + "text": "A Statement Reference is defined by the \"objectType\" of an \"object\" with value \"StatementRef\"", + "children": [ + { + "text": "statementref invalid when not \"StatementRef\"", + "children": [] + }, + { + "text": "substatement statementref invalid when not \"StatementRef\"", + "children": [] + } + ] + }, + { + "text": "A Statement Reference contains an \"id\" property", + "children": [ + { + "text": "statementref invalid when missing \"id\"", + "children": [] + }, + { + "text": "substatement statementref invalid when missing \"id\"", + "children": [] + } + ] + }, + { + "text": "A Statement Reference's \"id\" property is a UUID", + "children": [ + { + "text": "statementref \"id\" not \"uuid\"", + "children": [] + }, + { + "text": "substatement statementref \"id\" not \"uuid\"", + "children": [] + } + ] + } + ] + }, + { + "text": "Result Property Requirements", + "children": [ + { + "text": "A \"success\" property is a Boolean", + "children": [ + { + "text": "statement result \"success\" property is string \"true\"", + "children": [] + }, + { + "text": "statement result \"success\" property is string \"false\"", + "children": [] + }, + { + "text": "statement substatement result \"success\" property is string \"true\"", + "children": [] + }, + { + "text": "statement substatement result \"success\" property is string \"false\"", + "children": [] + } + ] + }, + { + "text": "A \"completion\" property is a Boolean", + "children": [ + { + "text": "statement result \"completion\" property is string \"true\"", + "children": [] + }, + { + "text": "statement result \"completion\" property is string \"false\"", + "children": [] + }, + { + "text": "statement substatement result \"completion\" property is string \"true\"", + "children": [] + }, + { + "text": "statement substatement result \"completion\" property is string \"false\"", + "children": [] + } + ] + }, + { + "text": "A \"response\" property is a String", + "children": [ + { + "text": "statement result \"response\" property is numeric", + "children": [] + }, + { + "text": "statement result \"completion\" property is object", + "children": [] + }, + { + "text": "statement substatement result \"completion\" property is numeric", + "children": [] + }, + { + "text": "statement substatement result \"completion\" property is object", + "children": [] + } + ] + }, + { + "text": "A \"duration\" property is a formatted to ISO 8601", + "children": [ + { + "text": "statement result \"duration\" property is invalid", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid", + "children": [] + } + ] + }, + { + "text": "An \"extensions\" property is an Object", + "children": [ + { + "text": "statement result \"extensions\" property is numeric", + "children": [] + }, + { + "text": "statement result \"extensions\" property is string", + "children": [] + }, + { + "text": "statement substatement result \"extensions\" property is numeric", + "children": [] + }, + { + "text": "statement substatement result \"extensions\" property is string", + "children": [] + } + ] + }, + { + "text": "A \"score\" property is an Object", + "children": [ + { + "text": "statement result score numeric", + "children": [] + }, + { + "text": "statement result score string", + "children": [] + }, + { + "text": "statement substatement result score numeric", + "children": [] + }, + { + "text": "statement substatement result score string", + "children": [] + } + ] + }, + { + "text": "A \"score\" Object's \"scaled\" property is a decimal number between -1 and 1, inclusive.", + "children": [ + { + "text": "statement result \"scaled\" accepts decimal", + "children": [] + }, + { + "text": "statement substatement result \"scaled\" accepts decimal", + "children": [] + }, + { + "text": "statement result \"scaled\" should pass with value 1.0", + "children": [] + }, + { + "text": "statement substatement result \"scaled\" pass with value -1.0000", + "children": [] + }, + { + "text": "statement result \"scaled\" should reject with value 1.01", + "children": [] + }, + { + "text": "statement substatement result \"scaled\" reject with value -1.00001", + "children": [] + } + ] + }, + { + "text": "A \"score\" Object's \"raw\" property is a decimal number between min and max, if present and otherwise unrestricted, inclusive", + "children": [ + { + "text": "statement result \"raw\" accepts decimal", + "children": [] + }, + { + "text": "statement substatement result \"raw\" accepts decimal", + "children": [] + }, + { + "text": "statement result \"raw\" rejects raw greater than max", + "children": [] + }, + { + "text": "statement substatement result \"raw\" rejects raw greater than max", + "children": [] + }, + { + "text": "statement result \"raw\" rejects raw less than min", + "children": [] + }, + { + "text": "statement substatement result \"raw\" rejects raw less than min", + "children": [] + } + ] + }, + { + "text": "A \"score\" Object's \"min\" property is a decimal number less than the \"max\" property, if it is present.", + "children": [ + { + "text": "statement result \"min\" accepts decimal", + "children": [] + }, + { + "text": "statement substatement result \"min\" accepts decimal", + "children": [] + }, + { + "text": "statement result \"min\" rejects decimal number greater than \"max\"", + "children": [] + }, + { + "text": "statement substatement result \"min\" rejects decimal number greater than \"max\"", + "children": [] + } + ] + }, + { + "text": "A \"score\" Object's \"max\" property is a Decimal accurate to seven significant decimal figures", + "children": [ + { + "text": "statement result \"max\" accepts a decimal number more than the \"min\" property, if it is present.", + "children": [] + }, + { + "text": "statement substatement result \"max\" accepts a decimal number more than the \"min\" property, if it is present.", + "children": [] + }, + { + "text": "statement result \"max\" accepts a decimal number more than the \"min\" property, if it is present.", + "children": [] + }, + { + "text": "statement substatement result \"max\" accepts a decimal number more than the \"min\" property, if it is present.", + "children": [] + } + ] + } + ] + }, + { + "text": "Context Property Requirements", + "children": [ + { + "text": "A \"registration\" property is a UUID", + "children": [ + { + "text": "statement context \"registration\" is object", + "children": [] + }, + { + "text": "statement context \"registration\" is string", + "children": [] + }, + { + "text": "statement substatement context \"registration\" is object", + "children": [] + }, + { + "text": "statement substatement context \"registration\" is string", + "children": [] + } + ] + }, + { + "text": "An \"instructor\" property is an Agent", + "children": [ + { + "text": "statement context \"instructor\" is object", + "children": [] + }, + { + "text": "statement context \"instructor\" is string", + "children": [] + }, + { + "text": "statement substatement context \"instructor\" is object", + "children": [] + }, + { + "text": "statement substatement context \"instructor\" is string", + "children": [] + } + ] + }, + { + "text": "An \"team\" property is a Group", + "children": [ + { + "text": "statement context \"team\" is agent", + "children": [] + }, + { + "text": "statement context \"team\" is object", + "children": [] + }, + { + "text": "statement context \"team\" is string", + "children": [] + }, + { + "text": "statement substatement context \"team\" is agent", + "children": [] + }, + { + "text": "statement substatement context \"team\" is object", + "children": [] + }, + { + "text": "statement substatement context \"team\" is string", + "children": [] + } + ] + }, + { + "text": "A \"contextActivities\" property is an Object", + "children": [ + { + "text": "statement context \"contextActivities\" is string", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is string", + "children": [] + } + ] + }, + { + "text": "A \"revision\" property is a String", + "children": [ + { + "text": "statement context \"revision\" is numeric", + "children": [] + }, + { + "text": "statement context \"revision\" is object", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is numeric", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is object", + "children": [] + } + ] + }, + { + "text": "A Statement cannot contain both a \"revision\" property in its \"context\" property and have the value of the \"object\" property's \"objectType\" be anything but \"Activity\"", + "children": [ + { + "text": "statement context \"revision\" is invalid with object agent", + "children": [] + }, + { + "text": "statement context \"revision\" is invalid with object group", + "children": [] + }, + { + "text": "statement context \"revision\" is invalid with statementref", + "children": [] + }, + { + "text": "statement context \"revision\" is invalid with substatement", + "children": [] + }, + { + "text": "statement context \"revision\" is valid with no ObjectType", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is invalid with object agent", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is invalid with object group", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is invalid with statementref", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is valid with no objectType", + "children": [] + } + ] + }, + { + "text": "A \"platform\" property is a String", + "children": [ + { + "text": "statement context \"platform\" is numeric", + "children": [] + }, + { + "text": "statement context \"platform\" is object", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is numeric", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is object", + "children": [] + } + ] + }, + { + "text": "A Statement cannot contain both a \"platform\" property in its \"context\" property and have the value of the \"object\" property's \"objectType\" be anything but \"Activity\"", + "children": [ + { + "text": "statement context \"platform\" is invalid with object agent", + "children": [] + }, + { + "text": "statement context \"platform\" is invalid with object group", + "children": [] + }, + { + "text": "statement context \"platform\" is invalid with statementref", + "children": [] + }, + { + "text": "statement context \"platform\" is invalid with substatement", + "children": [] + }, + { + "text": "statement context \"platform\" is valid with empty objectType", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is invalid with object agent", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is invalid with object group", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is invalid with statementref", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is valid with empty ObjectType", + "children": [] + } + ] + }, + { + "text": "A \"language\" property is a String", + "children": [ + { + "text": "statement context \"language\" is numeric", + "children": [] + }, + { + "text": "statement context \"language\" is object", + "children": [] + }, + { + "text": "statement substatement context \"language\" is numeric", + "children": [] + }, + { + "text": "statement substatement context \"language\" is object", + "children": [] + }, + { + "text": "statement context \"language\" is is invalid language", + "children": [] + }, + { + "text": "statement substatement context \"language\" is is invalid language", + "children": [] + } + ] + }, + { + "text": "A \"statement\" property is a Statement Reference", + "children": [ + { + "text": "statement context \"statement\" invalid with \"statementref\"", + "children": [] + }, + { + "text": "statement context \"statement\" invalid with \"id\" not UUID", + "children": [] + }, + { + "text": "statement substatement context \"statement\" invalid with \"statementref\"", + "children": [] + }, + { + "text": "statement substatement context \"statement\" invalid with \"id\" not UUID", + "children": [] + } + ] + }, + { + "text": "A \"contextActivities\" property's \"key\" has a value of \"parent\", \"grouping\", \"category\", or \"other\"", + "children": [ + { + "text": "statement context \"contextActivities\" is \"parent\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" is \"grouping\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" is \"category\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" is \"other\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" accepts all property keys \"parent\", \"grouping\", \"category\", and \"other\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" rejects any property key other than \"parent\", \"grouping\", \"category\", or \"other\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is \"parent\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is \"grouping\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is \"category\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is \"other\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" accepts all property keys \"parent\", \"grouping\", \"category\", and \"other\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" rejects any property key other than \"parent\", \"grouping\", \"category\", or \"other\"", + "children": [] + } + ] + }, + { + "text": "A \"contextActivities\" property's \"value\" is an Activity", + "children": [ + { + "text": "statement context \"contextActivities parent\" value is activity array", + "children": [] + }, + { + "text": "statement context \"contextActivities grouping\" value is activity array", + "children": [] + }, + { + "text": "statement context \"contextActivities category\" value is activity array", + "children": [] + }, + { + "text": "statement context \"contextActivities other\" value is activity array", + "children": [] + }, + { + "text": "statement context contextActivities property's value is activity array with activities", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities parent\" value is activity array", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities grouping\" value is activity array", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities category\" value is activity array", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities other\" value is activity array", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities property's value is activity array", + "children": [] + } + ] + }, + { + "text": "An LRS returns a ContextActivity in an array, even if only a single ContextActivity is returned", + "children": [ + { + "text": "should return array for statement context \"parent\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement context \"grouping\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement context \"category\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement context \"other\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement substatement context \"parent\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement substatement context \"grouping\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement substatement context \"category\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement substatement context \"other\" when single ContextActivity is passed", + "children": [] + } + ] + } + ] + }, + { + "text": "Timestamp Property Requirements", + "children": [ + { + "text": "A \"timestamp\" property is a TimeStamp", + "children": [ + { + "text": "statement \"template\" invalid string", + "children": [] + }, + { + "text": "statement \"template\" invalid date", + "children": [] + }, + { + "text": "statement \"template\" future date", + "children": [] + }, + { + "text": "substatement \"template\" invalid string", + "children": [] + }, + { + "text": "substatement \"template\" invalid date", + "children": [] + }, + { + "text": "substatement \"template\" future date", + "children": [] + } + ] + } + ] + }, + { + "text": "Stored Property Requirements", + "children": [ + { + "text": "An LRS MUST accept statements with the stored property", + "children": [ + { + "text": "using POST", + "children": [] + }, + { + "text": "using PUT", + "children": [] + } + ] + }, + { + "text": "A stored property must be a TimeStamp", + "children": [ + { + "text": "retrieve statements, test a stored property", + "children": [] + } + ] + } + ] + }, + { + "text": "Authority Property Requirements", + "children": [ + { + "text": "An LRS rejects with error code 400 Bad Request, a Request whose \"authority\" is a Group and consists of non-O-Auth Agents", + "children": [] + }, + { + "text": "An \"authority\" property is an Agent or Group", + "children": [ + { + "text": "should pass statement authority agent template", + "children": [] + }, + { + "text": "should pass statement authority template", + "children": [] + }, + { + "text": "should fail statement authority identified group (mbox)", + "children": [] + }, + { + "text": "should fail statement authority identified group", + "children": [] + }, + { + "text": "should fail statement authority identified group (openid)", + "children": [] + }, + { + "text": "should fail statement authority identified group (account)", + "children": [] + } + ] + }, + { + "text": "An \"authority\" property which is also a Group contains exactly two Agents", + "children": [ + { + "text": "statement \"authority\" invalid one member", + "children": [] + }, + { + "text": "statement \"authority\" invalid three member", + "children": [] + } + ] + }, + { + "text": "Statement authority shall only be an anonymous group with two members", + "children": [ + { + "text": "statement authority identified group is rejected", + "children": [] + }, + { + "text": "statement authority anonymous group with two members is accepted", + "children": [] + }, + { + "text": "statement authority anonymous group without two members is rejected", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request whose \"authority\" is a Group of more than two Agents", + "children": [ + { + "text": "statement \"authority\" invalid three member", + "children": [] + } + ] + }, + { + "text": "An LRS populates the \"authority\" property if it is not provided in the Statement, based on header information with the Agent corresponding to the user (contained within the header)", + "children": [ + { + "text": "should populate authority ", + "children": [] + } + ] + } + ] + }, + { + "text": "Version Property Requirements", + "children": [ + { + "text": "Statements returned by an LRS MUST retain the version property they are accepted with", + "children": [] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request which uses \"version\" and has the value set to anything but \"1.0\" or \"1.0.x\", where x is the semantic versioning number", + "children": [ + { + "text": "statement \"version\" valid 1.0", + "children": [] + }, + { + "text": "statement \"version\" valid 1.0.9", + "children": [] + }, + { + "text": "statement \"version\" invalid string", + "children": [] + }, + { + "text": "statement \"version\" invalid 0.9.9", + "children": [] + }, + { + "text": "statement \"version\" invalid 1.1.0", + "children": [] + } + ] + } + ] + }, + { + "text": "Attachments Property Requirements", + "children": [ + { + "text": "A Statement's \"attachments\" property is an array of Attachments", + "children": [ + { + "text": "statement \"attachments\" is an array", + "children": [] + }, + { + "text": "statement \"attachments\" not an array", + "children": [] + } + ] + }, + { + "text": "An Attachment is an Object", + "children": [ + { + "text": "statement \"attachment\" invalid numeric", + "children": [] + }, + { + "text": "statement \"attachment\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"usageType\" property is an IRI", + "children": [ + { + "text": "statement \"usageType\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"contentType\" property is an Internet Media/MIME type", + "children": [ + { + "text": "statement \"contentType\" invalid number", + "children": [] + } + ] + }, + { + "text": "A \"length\" property is an Integer", + "children": [ + { + "text": "statement \"length\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"sha2\" property is a String", + "children": [ + { + "text": "statement \"sha2\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"fileUrl\" property is an IRL", + "children": [ + { + "text": "statement \"fileUrl\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"display\" property is a Language Map", + "children": [ + { + "text": "statement attachment \"display\" language map numeric", + "children": [] + }, + { + "text": "statement attachment \"display\" language map string", + "children": [] + }, + { + "text": "statement attachment \"description\" language map numeric", + "children": [] + }, + { + "text": "statement attachment \"description\" language map string", + "children": [] + } + ] + } + ] + }, + { + "text": "Retrieval of Statements", + "children": [ + { + "text": "A \"statements\" property which is too large for a single page will create a container for each additional page", + "children": [] + }, + { + "text": "A \"more\" property's referenced container object follows the same rules as the original GET request, originating with a single \"statements\" property and a single \"more\" property", + "children": [] + }, + { + "text": "An LRS's Statement API, upon processing a successful GET request, will return a single \"statements\" property and a single \"more\" property.", + "children": [ + { + "text": "will return single statements property and may return", + "children": [] + } + ] + }, + { + "text": "A \"statements\" property is an Array of Statements", + "children": [ + { + "text": "should return StatementResult with statements as array using GET without \"statementId\" or \"voidedStatementId\"", + "children": [] + } + ] + }, + { + "text": "The \"more\" property is absent or an empty string (no whitespace) if the entire results of the original GET request have been returned.", + "children": [ + { + "text": "should return empty \"more\" property or no \"more\" property when all statements returned", + "children": [] + } + ] + }, + { + "text": "If not empty, the \"more\" property's IRL refers to a specific container object corresponding to the next page of results from the orignal GET request", + "children": [ + { + "text": "should return \"more\" which refers to next page of results", + "children": [] + } + ] + } + ] + }, + { + "text": "Signed Statements", + "children": [ + { + "text": "LRS must validate and store statement signatures if they are provided", + "children": [ + { + "text": "A Signed Statement MUST include a JSON web signature, JWS", + "children": [ + { + "text": "rejects a signed statement with a malformed signature - bad content type", + "children": [] + }, + { + "text": "rejects a signed statement with a malformed signature - bad JWS", + "children": [] + } + ] + }, + { + "text": "The JWS signature MUST have a payload of a valid JSON serialization of the complete Statement before the signature was added.", + "children": [ + { + "text": "rejects statement with invalid JSON serialization", + "children": [] + } + ] + }, + { + "text": "The JWS signature MUST use an algorithm of \"RS256\", \"RS384\", or \"RS512\".", + "children": [ + { + "text": "Accepts signed statement with \"RS256\"", + "children": [] + }, + { + "text": "Accepts signed statement with \"RS384\"", + "children": [] + }, + { + "text": "Accepts signed statement with \"RS512\"", + "children": [] + }, + { + "text": "Rejects signed statement with another algorithm", + "children": [] + } + ] + } + ] + } + ] + }, + { + "text": "Special Data Types and Rules", + "children": [ + { + "text": "An Extension is defined as an Object of any \"extensions\" property", + "children": [ + { + "text": "statement activity extensions valid boolean", + "children": [] + }, + { + "text": "statement activity extensions valid numeric", + "children": [] + }, + { + "text": "statement activity extensions valid object", + "children": [] + }, + { + "text": "statement activity extensions valid string", + "children": [] + }, + { + "text": "statement result extensions valid boolean", + "children": [] + }, + { + "text": "statement result extensions valid numeric", + "children": [] + }, + { + "text": "statement result extensions valid object", + "children": [] + }, + { + "text": "statement result extensions valid string", + "children": [] + }, + { + "text": "statement context extensions valid boolean", + "children": [] + }, + { + "text": "statement context extensions valid numeric", + "children": [] + }, + { + "text": "statement context extensions valid object", + "children": [] + }, + { + "text": "statement context extensions valid string", + "children": [] + }, + { + "text": "statement substatement activity extensions valid boolean", + "children": [] + }, + { + "text": "statement substatement activity extensions valid numeric", + "children": [] + }, + { + "text": "statement substatement activity extensions valid object", + "children": [] + }, + { + "text": "statement substatement activity extensions valid string", + "children": [] + }, + { + "text": "statement substatement result extensions valid boolean", + "children": [] + }, + { + "text": "statement substatement result extensions valid numeric", + "children": [] + }, + { + "text": "statement substatement result extensions valid object", + "children": [] + }, + { + "text": "statement substatement result extensions valid string", + "children": [] + }, + { + "text": "statement substatement context extensions valid boolean", + "children": [] + }, + { + "text": "statement substatement context extensions valid numeric", + "children": [] + }, + { + "text": "statement substatement context extensions valid object", + "children": [] + }, + { + "text": "statement substatement context extensions valid string", + "children": [] + } + ] + }, + { + "text": "An Extension can be null, an empty string, objects with nothing in them when using POST.", + "children": [ + { + "text": "statement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement activity extension values can be empty string", + "children": [] + }, + { + "text": "statement activity extension values can be null", + "children": [] + }, + { + "text": "statement activity extension values can be empty object", + "children": [] + }, + { + "text": "statement result extensions can be empty object", + "children": [] + }, + { + "text": "statement result extension values can be empty string", + "children": [] + }, + { + "text": "statement result extension values can be null", + "children": [] + }, + { + "text": "statement result extension values can be empty object", + "children": [] + }, + { + "text": "statement context extensions can be empty object", + "children": [] + }, + { + "text": "statement context extensions can be empty string", + "children": [] + }, + { + "text": "statement context extensions can be null", + "children": [] + }, + { + "text": "statement context extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement activity extension values can be empty string", + "children": [] + }, + { + "text": "statement substatement activity extension values can be null", + "children": [] + }, + { + "text": "statement substatement activity extension values can be empty object", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty string", + "children": [] + }, + { + "text": "statement substatement result extensions can be null", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty string", + "children": [] + }, + { + "text": "statement substatement context extensions can be null", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty object", + "children": [] + } + ] + }, + { + "text": "An Extension \"key\" is an IRI", + "children": [ + { + "text": "statement activity extensions key is not an IRI", + "children": [] + }, + { + "text": "statement result extensions key is not an IRI", + "children": [] + }, + { + "text": "statement context extensions key is not an IRI", + "children": [] + }, + { + "text": "statement substatement activity extensions key is not an IRI", + "children": [] + }, + { + "text": "statement substatement result extensions key is not an IRI", + "children": [] + }, + { + "text": "statement substatement context extensions key is not an IRI", + "children": [] + } + ] + }, + { + "text": "An Extension can be null, an empty string, objects with nothing in them when using PUT.", + "children": [ + { + "text": "statement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement activity extension values can be empty string", + "children": [] + }, + { + "text": "statement activity extension values can be null", + "children": [] + }, + { + "text": "statement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement result extensions can be empty object", + "children": [] + }, + { + "text": "statement result extension values can be empty string", + "children": [] + }, + { + "text": "statement result extension values can be null", + "children": [] + }, + { + "text": "statement result extension values can be empty object", + "children": [] + }, + { + "text": "statement context extensions can be empty object", + "children": [] + }, + { + "text": "statement context extension values can be empty string", + "children": [] + }, + { + "text": "statement context extension values can be null", + "children": [] + }, + { + "text": "statement context extension values can be empty object", + "children": [] + }, + { + "text": "statement substatement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement activity extension values can be empty string", + "children": [] + }, + { + "text": "statement substatement activity extension values can be null", + "children": [] + }, + { + "text": "statement substatement activity extension values can be empty object", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement result extension values can be empty string", + "children": [] + }, + { + "text": "statement substatement result extension values can be null", + "children": [] + }, + { + "text": "statement substatement result extension values can be empty object", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement context extension values can be empty string", + "children": [] + }, + { + "text": "statement substatement context extension values can be null", + "children": [] + }, + { + "text": "statement substatement context extension values can be empty object", + "children": [] + } + ] + }, + { + "text": "A Language Map follows RFC5646", + "children": [ + { + "text": "statement verb \"display\" language map invalid", + "children": [] + }, + { + "text": "statement object \"name\" language map invalid", + "children": [] + }, + { + "text": "statement object \"description\" language map invalid", + "children": [] + }, + { + "text": "statement attachment \"display\" language map invalid", + "children": [] + }, + { + "text": "statement attachment \"description\" language map invalid", + "children": [] + }, + { + "text": "statement substatement verb \"display\" language map invalid", + "children": [] + }, + { + "text": "statement substatement activity \"name\" language map invalid", + "children": [] + }, + { + "text": "statement substatement activity \"description\" language map invalid", + "children": [] + } + ] + }, + { + "text": "A TimeStamp is defined as a Date/Time formatted according to ISO 8601", + "children": [ + { + "text": "statement \"template\" invalid string in timestamp", + "children": [] + }, + { + "text": "statement \"template\" invalid date in timestamp", + "children": [] + }, + { + "text": "statement \"template\" invalid date in timestamp: did not reject statement timestmap with -00 offset", + "children": [] + }, + { + "text": "statement \"template\" invalid date in timestamp: did not reject statement timestmap with -0000 offset", + "children": [] + }, + { + "text": "statement \"template\" invalid date in timestamp: did not reject statement timestmap with -00:00 offset", + "children": [] + }, + { + "text": "substatement \"template\" invalid string in timestamp", + "children": [] + }, + { + "text": "substatement \"template\" invalid date in timestamp", + "children": [] + }, + { + "text": "substatement \"template\" invalid date in timestamp: did not reject substatement timestamp with -00 offset", + "children": [] + }, + { + "text": "substatement \"template\" invalid date in timestamp: did not reject substatement timestamp with -0000 offset", + "children": [] + }, + { + "text": "substatement \"template\" invalid date in timestamp: did not reject substatement timestamp with -00:00 offset", + "children": [] + } + ] + }, + { + "text": "A Timestamp MUST preserve precision to at least milliseconds, 3 decimal points beyond seconds.", + "children": [ + { + "text": "retrieve statements, test a timestamp property", + "children": [] + }, + { + "text": "retrieve statements, test a stored property", + "children": [] + } + ] + }, + { + "text": "A Duration MUST be expressed using the format for Duration in ISO 8601:2004(E) section 4.4.3.2.", + "children": [ + { + "text": "statement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with invalid string", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with invalid number", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid invalid number", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with invalid object", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid with invalid object", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with invalid duration", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid with invalid duration", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid with \"PT4H35M59.14S\"", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid with \"PT16559.14S\"", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid with \"P3Y1M29DT4H35M59.14S\"", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid with \"P3Y\"", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid with \"P4W\"", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with \"P4W1D\"", + "children": [] + } + ] + } + ] + }, + { + "text": "HEAD Request Implementation Requirements", + "children": [ + { + "text": "An LRS accepts HEAD requests without Content-Length headers", + "children": [] + }, + { + "text": "An LRS accepts GET requests without Content-Length headers", + "children": [] + }, + { + "text": "An LRS accepts HEAD requests", + "children": [ + { + "text": "should succeed HEAD activities with no body", + "children": [] + }, + { + "text": "should succeed HEAD activities profile with no body", + "children": [] + }, + { + "text": "should succeed HEAD activities state with no body", + "children": [] + }, + { + "text": "should succeed HEAD agents with no body", + "children": [] + }, + { + "text": "should succeed HEAD agents profile with no body", + "children": [] + }, + { + "text": "should succeed HEAD statements with no body", + "children": [] + } + ] + }, + { + "text": "An LRS responds to a HEAD request in the same way as a GET request, but without the message-body", + "children": [ + { + "text": "should succeed HEAD activities with no body", + "children": [] + }, + { + "text": "should succeed HEAD activities profile with no body", + "children": [] + }, + { + "text": "should succeed HEAD activities state with no body", + "children": [] + }, + { + "text": "should succeed HEAD agents with no body", + "children": [] + }, + { + "text": "should succeed HEAD agents profile with no body", + "children": [] + }, + { + "text": "should succeed HEAD statements with no body", + "children": [] + } + ] + } + ] + }, + { + "text": "Alternate Request Syntax Requirements", + "children": [ + { + "text": "The LRS MUST support the Alternate Request Syntax", + "children": [ + { + "text": "An LRS accepts a valid POST request containing a GET request returning 200 OK and the StatementResult Object.", + "children": [] + }, + { + "text": "An LRS rejects an alternate request syntax not issued as a POST", + "children": [] + }, + { + "text": "An LRS accepts an alternate request syntax PUT issued as a POST", + "children": [] + }, + { + "text": "During an alternate request syntax the LRS treats the listed form parameters, 'Authorization', 'X-Experience-API-Version', 'Content-Type', 'Content-Length', 'If-Match' and 'If-None-Match', as header parameters", + "children": [] + }, + { + "text": "An LRS will reject an alternate request syntax which contains any extra information with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS will reject an alternate request syntax sending content which does not have a form parameter with the name of \"content\"", + "children": [ + { + "text": "will pass PUT with content body which is url encoded", + "children": [] + }, + { + "text": "will fail PUT with no content body", + "children": [] + }, + { + "text": "will fail PUT with content body which is not url encoded", + "children": [] + } + ] + } + ] + } + ] + }, + { + "text": "Encoding Requirements", + "children": [ + { + "text": "All Strings are encoded and interpreted as UTF-8", + "children": [] + } + ] + }, + { + "text": "Content Type Requirements", + "children": [ + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and for any part except the first does not have a Header named \"Content-Transfer-Encoding\" with a value of \"binary\"", + "children": [] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request which uses Attachments and does not have a \"Content-Type\" header with value \"application/json\" or \"multipart/mixed\"", + "children": [ + { + "text": "should succeed when attachment uses \"fileUrl\" and request content-type is \"application/json\"", + "children": [] + }, + { + "text": "should fail when attachment uses \"fileUrl\" and request content-type is \"multipart/form-data\"", + "children": [] + }, + { + "text": "should succeed when attachment is raw data and request content-type is \"multipart/mixed\"", + "children": [] + }, + { + "text": "should fail when attachment is raw data and request content-type is \"multipart/form-data\"", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which has excess multi-part sections that are not attachments.", + "children": [ + { + "text": "should fail when passing statement attachments with excess multipart sections", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content-Type\" header with value \"application/json\", and has a discrepancy in the number of Attachments vs. the number of fileURL members", + "children": [ + { + "text": "should fail when passing statement attachments and missing attachment\"s binary", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and does not have a body header named \"boundary\"", + "children": [ + { + "text": "should fail if boundary not provided in body", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and does not have a Boundary before each \"Content-Type\" header", + "children": [ + { + "text": "should fail if boundary not provided in body", + "children": [] + }, + { + "text": "should fail if boundary not provided in header", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and does not the first document part with a \"Content-Type\" header with a value of \"application/json\"", + "children": [ + { + "text": "should fail when attachment is raw data and first part content type is not \"application/json\"", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and does not have all of the Statements in the first document part", + "children": [ + { + "text": "should fail when statements separated into multiple parts", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and for any part except the first does not have a Header named \"X-Experience-API-Hash\" with a value of one of those found in a \"sha2\" property of a Statement in the first part of this document", + "children": [ + { + "text": "should fail when attachments missing header \"X-Experience-API-Hash\"", + "children": [] + }, + { + "text": "should fail when attachments header \"X-Experience-API-Hash\" does not match \"sha2\"", + "children": [] + } + ] + } + ] + }, + { + "text": "Statement Resource Requirements", + "children": [ + { + "text": "An LRS has a Statement Resource with endpoint \"base IRI\"+\"/statements\"", + "children": [ + { + "text": "should allow \"/statements\" POST", + "children": [] + }, + { + "text": "should allow \"/statements\" PUT", + "children": [] + }, + { + "text": "should allow \"/statements\" GET", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful PUT request returns code 204 No Content", + "children": [ + { + "text": "should persist statement and return status 204", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource accepts PUT requests only if it contains a \"statementId\" parameter", + "children": [ + { + "text": "should persist statement using \"statementId\" parameter", + "children": [] + }, + { + "text": "should fail without using \"statementId\" parameter", + "children": [] + } + ] + }, + { + "text": "An LRS cannot modify a Statement, state, or Object in the event it receives a Statement with statementID equal to a Statement in the LRS already.", + "children": [ + { + "text": "should not update statement with matching \"statementId\" on PUT", + "children": [] + }, + { + "text": "should not update statement with matching \"statementId\" on POST", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource accepts POST requests", + "children": [ + { + "text": "should persist statement using \"POST\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful POST request returns code 200 OK and all Statement UUIDs within the POST **Implicit**", + "children": [ + { + "text": "should persist statement using \"POST\" and return array of IDs", + "children": [] + } + ] + }, + { + "text": "LRS's Statement Resource accepts GET requests", + "children": [ + { + "text": "should return using GET", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful GET request with a \"statementId\" parameter, returns code 200 OK and a single Statement with the corresponding \"id\".", + "children": [ + { + "text": "should retrieve statement using \"statementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful GET request with a \"voidedStatementId\" parameter, returns code 200 OK and a single Statement with the corresponding \"id\".", + "children": [ + { + "text": "should return a voided statement when using GET \"voidedStatementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful GET request with neither a \"statementId\" nor a \"voidedStatementId\" parameter, returns code 200 OK and a StatementResult Object.", + "children": [ + { + "text": "should return StatementResult using GET without \"statementId\" or \"voidedStatementId\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"agent\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"verb\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"activity\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"registration\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"related_activities\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"related_agents\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"since\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"until\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"limit\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"ascending\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"format\"", + "children": [] + }, + { + "text": "should return multipart response format StatementResult using GET with \"attachments\" parameter as true", + "children": [] + }, + { + "text": "should not return multipart response format using GET with \"attachments\" parameter as false", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"statementId\" as a parameter", + "children": [ + { + "text": "should process using GET with \"statementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"voidedStatementId\" as a parameter", + "children": [ + { + "text": "should process using GET with \"voidedStatementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"agent\" as a parameter", + "children": [ + { + "text": "should process using GET with \"agent\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"verb\" as a parameter", + "children": [ + { + "text": "should process using GET with \"verb\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"activity\" as a parameter", + "children": [ + { + "text": "should process using GET with \"activity\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"registration\" as a parameter", + "children": [ + { + "text": "should process using GET with \"registration\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"related_activities\" as a parameter", + "children": [ + { + "text": "should process using GET with \"related_activities\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"related_agents\" as a parameter", + "children": [ + { + "text": "should process using GET with \"related_agents\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"since\" as a parameter", + "children": [ + { + "text": "should process using GET with \"since\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"until\" as a parameter", + "children": [ + { + "text": "should process using GET with \"until\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"limit\" as a parameter", + "children": [ + { + "text": "should process using GET with \"limit\"", + "children": [] + } + ] + }, + { + "text": "If the \"Accept-Language\" header is present as part of the GET request to the Statement API and the \"format\" parameter is set to \"canonical\", the LRS MUST apply this data to choose the matching language in the response.", + "children": [ + { + "text": "should apply this data to choose the matching language in the response", + "children": [] + }, + { + "text": "should NOT apply this data to choose the matching language in the response when format is not set ", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"format\" as a parameter", + "children": [ + { + "text": "should process using GET with \"format\" absent", + "children": [] + }, + { + "text": "should process using GET with \"format\" canonical", + "children": [] + }, + { + "text": "should process using GET with \"format\" exact", + "children": [] + }, + { + "text": "should process using GET with \"format\" ids", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"attachments\" as a parameter", + "children": [ + { + "text": "should process using GET with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRSs Statement Resource, upon receiving a GET request, MUST have a \"Content-Type\" header", + "children": [ + { + "text": "should contain the content-type header", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"ascending\" as a parameter", + "children": [ + { + "text": "should process using GET with \"ascending\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource rejects with error code 400 a GET request with both \"statementId\" and anything other than \"attachments\" or \"format\" as parameters", + "children": [ + { + "text": "should fail when using \"statementId\" with \"agent\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"verb\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"activity\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"registration\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"related_activities\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"related_agents\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"since\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"until\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"limit\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"ascending\"", + "children": [] + }, + { + "text": "should pass when using \"statementId\" with \"format\"", + "children": [] + }, + { + "text": "should pass when using \"statementId\" with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource rejects with error code 400 a GET request with both \"voidedStatementId\" and anything other than \"attachments\" or \"format\" as parameters", + "children": [ + { + "text": "should fail when using \"voidedStatementId\" with \"agent\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"verb\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"activity\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"registration\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"related_activities\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"related_agents\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"since\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"until\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"limit\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"ascending\"", + "children": [] + }, + { + "text": "should pass when using \"voidedStatementId\" with \"format\"", + "children": [] + }, + { + "text": "should pass when using \"voidedStatementId\" with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "The LRS will NOT reject a GET request which returns an empty \"statements\" property", + "children": [ + { + "text": "should return empty array list", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a GET request, returns a header with name \"X-Experience-API-Consistent-Through\" regardless of the code returned.", + "children": [ + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" misusing GET", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"agent\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"verb\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"activity\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"registration\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"related_activities\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"related_agents\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"since\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"until\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"limit\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"ascending\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"format\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRS's \"X-Experience-API-Consistent-Through\" header is an ISO 8601 combined date and time", + "children": [ + { + "text": "should return valid \"X-Experience-API-Consistent-Through\" using GET", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"agent\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"verb\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"activity\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"registration\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"related_activities\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"related_agents\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"since\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"until\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"limit\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"ascending\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"format\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRSs Statement Resource does not return attachment data and only returns application/json if the \"attachment\" parameter set to \"false\"", + "children": [ + { + "text": "should NOT return the attachment if \"attachments\" is missing", + "children": [] + }, + { + "text": "should NOT return the attachment if \"attachments\" is false", + "children": [] + }, + { + "text": "should return the attachment when \"attachment\" is true", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource, upon processing a successful GET request, can only return a Voided Statement if that Statement is specified in the voidedStatementId parameter of that request", + "children": [ + { + "text": "should not return a voided statement if using GET \"statementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource, upon processing a successful GET request wishing to return a Voided Statement still returns Statements which target it", + "children": [ + { + "text": "should only return statements stored after designated \"since\" timestamp when using \"since\" parameter", + "children": [] + }, + { + "text": "should only return statements stored at or before designated \"before\" timestamp when using \"until\" parameter", + "children": [] + }, + { + "text": "should return the number of statements listed in \"limit\" parameter", + "children": [] + }, + { + "text": "should return StatementRef and voiding statement when not using \"since\", \"until\", \"limit\"", + "children": [] + } + ] + }, + { + "text": "The Statements within the \"statements\" property will correspond to the filtering criterion sent in with the GET request", + "children": [ + { + "text": "should return StatementResult with statements as array using GET with \"agent\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"verb\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"activity\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"registration\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"related_activities\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"related_agents\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"since\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"until\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"limit\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"ascending\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"format\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"attachments\"", + "children": [] + } + ] + } + ] + }, + { + "text": "Document Resource Requirements", + "children": [ + { + "text": "An LRS makes no modifications to stored data for any rejected request", + "children": [] + }, + { + "text": "A Document Merge overwrites any duplicate Objects from the previous document with the new document.", + "children": [] + }, + { + "text": "A Document Merge only performs overwrites at one level deep, although the entire object is replaced.", + "children": [] + } + ] + }, + { + "text": "State Resource Requirements", + "children": [ + { + "text": "An LRS has a State Resource with endpoint \"base IRI\"+\"/activities/state\"", + "children": [] + }, + { + "text": "An LRS's State Resource accepts PUT requests", + "children": [] + }, + { + "text": "An LRS's State Resource accepts POST requests", + "children": [] + }, + { + "text": "An LRS's State Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's State Resource accepts DELETE requests", + "children": [] + }, + { + "text": "An LRS's State Resource upon processing a successful GET request with a valid \"stateId\" as a parameter returns the document satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's State Resource upon processing a successful DELETE request with a valid \"stateId\" as a parameter deletes the document satisfying the requirements of the DELETE and returns code 204 No Content", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a PUT request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a POST request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a GET request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a DELETE request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a PUT request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a PUT request with \"agent\" as a parameter if it is not in JSON format with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a POST request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a GET request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a DELETE request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource can process a PUT request with \"registration\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource, rejects a POST request if the document is found and either document's type is not \"application/json\" with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document", + "children": [] + }, + { + "text": "An LRS's State Resource performs a Document Merge if a document is found and both it and the document in the POST request have type \"application/json\"", + "children": [] + }, + { + "text": "An LRS must reject with 400 Bad Request a POST request to the State Resource which contains name/value pairs with invalid JSON and the Content-Type header is 'application/json'", + "children": [] + }, + { + "text": "An LRS's State Resource can process a POST request with \"registration\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource can process a GET request with \"registration\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource can process a DELETE request with \"registration\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a PUT request without \"stateId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a POST request without \"stateId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource can process a GET request with \"stateId\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource can process a GET request with \"since\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a GET request with \"since\" as a parameter if it is not a \"TimeStamp\", with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource can process a DELETE request with \"stateId\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource upon processing a successful GET request without \"stateId\" as a parameter returns an array of ids of state data documents satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's returned array of ids from a successful GET request to the State Resource all refer to documents stored after the TimeStamp in the \"since\" parameter of the GET request", + "children": [] + }, + { + "text": "An LRS's State Resource upon processing a successful DELETE request without \"stateId\" as a parameter deletes documents satisfying the requirements of the DELETE and code 204 No Content", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a POST request with \"agent\" as a parameter if it is not in JSON format with error code 400 Bad Request", + "children": [ + { + "text": "Should reject POST State with agent invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a GET request with \"agent\" as a parameter if it is not in JSON format with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a DELETE request with \"agent\" as a parameter if it is not in JSON format with error code 400 Bad Request", + "children": [ + { + "text": "Should reject DELETE with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a PUT request with \"registration\" as a parameter if it is not a UUID with error code 400 Bad Request", + "children": [ + { + "text": "Should reject PUT with \"registration\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRSs State Resource, rejects a POST request if the document is found and either document is not a valid JSON Object", + "children": [ + { + "text": "If the document being posted to the State Resource does not have a Content-Type of application/json and the existing document does, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the existing document does not have a Content-Type of application/json but the document being posted to the State Resource does the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the document being posted to the State Resource has a content type of Content-Type of application/json but cannot be parsed as a JSON Object, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a POST request with \"registration\" as a parameter if it is not a UUID with error code 400 Bad Request", + "children": [ + { + "text": "Should reject POST with \"registration\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a GET request with \"registration\" as a parameter if it is not a UUID with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"registration\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a DELETE request with \"registration\" as a parameter if it is not a UUID with error code 400 Bad Request", + "children": [ + { + "text": "Should reject DELETE with \"registration\" with invalid value", + "children": [] + } + ] + } + ] + }, + { + "text": "Agents Resource Requirements", + "children": [ + { + "text": "An LRS has an Agents Resource with endpoint \"base IRI\" + /agents\"", + "children": [] + }, + { + "text": "An LRS's Agents Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's Agent Resource upon processing a successful GET request returns a Person Object if the \"agent\" parameter can be found in the LRS and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Agents Resource rejects a GET request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "A Person Object's \"objectType\" property is a String and is \"Person\"", + "children": [] + }, + { + "text": "A Person Object's \"name\" property is an Array of Strings", + "children": [] + }, + { + "text": "A Person Object's \"mbox\" property is an Array of IRIs", + "children": [] + }, + { + "text": "A Person Object's \"mbox\" entries have the form \"mailto:emailaddress\"", + "children": [] + }, + { + "text": "A Person Object's \"mbox_sha1sum\" property is an Array of Strings", + "children": [] + }, + { + "text": "A Person Object's \"openid\" property is an Array of Strings", + "children": [] + }, + { + "text": "A Person Object's \"account\" property is an Array of Account Objects", + "children": [] + }, + { + "text": "An LRSs Agents Resource rejects a GET request with \"agent\" as a parameter if it is not a valid, in structure, Agent with error code 400 Bad Request", + "children": [] + } + ] + }, + { + "text": "Activities Resource Requirements", + "children": [ + { + "text": "An LRS has an Activities Resource with endpoint \"base IRI\" + /activities\"", + "children": [] + }, + { + "text": "An LRS's Activities Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's Activities Resource upon processing a successful GET request returns the complete Activity Object", + "children": [] + }, + { + "text": "An LRS's Activities Resource rejects a GET request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activities Resource rejects a GET request with \"activityId\" as a parameter if it is not type \"String\" with error code 400 Bad Request", + "children": [] + }, + { + "text": "The Activity Object must contain all available information about an activity from any statements who target the same \"activityId\". For example, LRS accepts two statements each with a different language description of an activity using the exact same \"activityId\". The LRS must return both language descriptions when a GET request is made to the Activities endpoint for that \"activityId\"", + "children": [] + } + ] + }, + { + "text": "Agent Profile Resource Requirements", + "children": [ + { + "text": "An LRS's Agent Profile Resource upon processing a successful GET request with a valid \"profileId\" as a parameter returns the document satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a PUT request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a POST request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a POST request with \"agent\" as a parameter if it is not an Agent Object with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a DELETE request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a PUT request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a POST request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a DELETE request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource upon processing a successful GET request without \"profileId\" as a parameter returns an array of ids of agent profile documents satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a GET request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource can process a GET request with \"since\" as a parameter", + "children": [] + }, + { + "text": "An LRS's returned array of ids from a successful GET request to the Agent Profile Resource all refer to documents stored after the TimeStamp in the \"since\" parameter of the GET request if such a parameter was present", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource performs a Document Merge if a document is found and both it and the document in the POST request have type \"application/json\"", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource, rejects a POST request if the document is found and either document is not a valid JSON Object", + "children": [] + }, + { + "text": "An LRS must reject with 400 Bad Request a POST request to the Activitiy Profile Resource which contains name/value pairs with invalid JSON and the Content-Type header is 'application/json'", + "children": [] + }, + { + "text": "An LRS has an Agent Profile Resource with endpoint \"base IRI\"+\"/agents/profile\"", + "children": [ + { + "text": "An LRS's Agent Profile Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource upon processing a successful PUT request returns code 204 No Content", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource upon processing a PUT request without an ETag header returns an error code and message", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource upon processing a successful POST request returns code 204 No Content", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource upon processing a successful DELETE request deletes the associated profile and returns code 204 No Content", + "children": [] + } + ] + }, + { + "text": "An LRS's Agent Profile Resource rejects a PUT request with \"agent\" as a parameter if it is not an Agent Object with error code 400 Bad Request", + "children": [ + { + "text": "Should reject PUT with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's Agent Profile Resource rejects a DELETE request with \"agent\" as a parameter if it is not an Agent Object with error code 400 Bad Request", + "children": [ + { + "text": "Should reject DELETE with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's Agent Profile Resource rejects a GET request with \"agent\" as a parameter if it is a valid, in structure, Agent with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's Agent Profile Resource rejects a GET request with \"since\" as a parameter if it is not a \"TimeStamp\", with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"since\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRSs Agent Profile Resource, rejects a POST request if the document is found and either documents type is not \"application/json\" with error code 400 Bad Request", + "children": [ + { + "text": "If the document being posted to the Agent Profile Resource does not have a Content-Type of application/json and the existing document does, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the existing document does not have a Content-Type of application/json but the document being posted to the Agent Profile Resource does the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the document being posted to the Agent Profile Resource has a content type of Content-Type of application/json but cannot be parsed as a JSON Object, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + } + ] + } + ] + }, + { + "text": "Activity Profile Resource Requirements", + "children": [ + { + "text": "An LRS has an Activity Profile Resource with endpoint \"base IRI\"+\"/activities/profile\"", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource accepts POST requests", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource accepts DELETE requests", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource upon processing a successful GET request with a valid \"profileId\" as a parameter returns the document satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a PUT request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a POST request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a DELETE request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a GET request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a PUT request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a POST request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a DELETE request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource upon processing a successful GET request without \"profileId\" as a parameter returns an array of ids of activity profile documents satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource can process a GET request with \"since\" as a parameter", + "children": [] + }, + { + "text": "An LRS's returned array of ids from a successful GET request to the Activity Profile Resource all refer to documents stored after the TimeStamp in the \"since\" parameter of the GET request if such a parameter was present", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource performs a Document Merge if a document is found and both it and the document in the POST request have type \"application/json\"", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource, rejects a POST request if the document is found and either document's type is not \"application/json\" with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's must reject, with 400 Bad Request, a POST request to the Activity Profile Resource which contains name/value pairs with invalid JSON and the Content-Type header is \"application/json\"", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource accepts PUT requests", + "children": [ + { + "text": "passes with 204 no content", + "children": [] + }, + { + "text": "fails without ETag header", + "children": [] + } + ] + }, + { + "text": "An LRS's Activity Profile Resource rejects a GET request with \"since\" as a parameter if it is not a \"TimeStamp\", with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"since\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's Activity Profile Resource, rejects a POST request if the document is found and either document is not a valid JSON Object", + "children": [ + { + "text": "If the document being posted to the Activity Profile Resource does not have a Content-Type of application/json and the existing document does, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the existing document does not have a Content-Type of application/json but the document being posted to the Activity Profile Resource does the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the document being posted to the Activity Profile Resource has a content type of Content-Type of application/json but cannot be parsed as a JSON Object, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + } + ] + } + ] + }, + { + "text": "About Resource Requirements", + "children": [ + { + "text": "An LRS has an About Resource with endpoint \"base IRI\"+\"/about\"", + "children": [] + }, + { + "text": "An LRS's About Resource upon processing a successful GET request returns a version property and code 200 OK", + "children": [] + }, + { + "text": "An LRS's About Resource's version property is an array of strings", + "children": [] + }, + { + "text": "An LRS's About Resource's version property contains at least one string of \"1.0.3\"", + "children": [] + }, + { + "text": "An LRS's About Resource's version property can only have values of \"0.9\", \"0.95\", \"1.0.0\", or \"\"1.0.\" + X\" with", + "children": [] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request which does not use a \"X-Experience-API-Version\" header name to any Resource except the About Resource", + "children": [ + { + "text": "using Statement Endpoint", + "children": [] + }, + { + "text": "using Activities Endpoint", + "children": [] + }, + { + "text": "using Activities Profile Endpoint", + "children": [] + }, + { + "text": "using Activities State Endpoint", + "children": [] + }, + { + "text": "using Agents Endpoint", + "children": [] + }, + { + "text": "using Agents Profile Endpoint", + "children": [] + } + ] + } + ] + }, + { + "text": "Concurrency Requirements", + "children": [ + { + "text": "An LRS must support HTTP/1.1 entity tags (ETags) to implement optimistic concurrency control when handling Resources where PUT may overwrite existing data", + "children": [ + { + "text": "When responding to a GET request to Agent Profile resource, include an ETag HTTP header in the response", + "children": [] + }, + { + "text": "When responding to a GET request to Activities Profile resource, include an ETag HTTP header in the response", + "children": [] + }, + { + "text": "When returning an ETag header, the value should be calculated as a SHA1 hexadecimal value", + "children": [] + }, + { + "text": "When responding to a GET Request the Etag header must be enclosed in quotes", + "children": [] + }, + { + "text": "With a valid etag", + "children": [] + }, + { + "text": "When responding to a PUT request, handle the If-None-Match header as described in RFC 2616, HTTP/1.1 if it contains \"*\"", + "children": [ + { + "text": "succeeds when no document exists", + "children": [] + }, + { + "text": "rejects if a document already exists", + "children": [] + } + ] + }, + { + "text": "If Header precondition in PUT Requests for RFC2616 fail", + "children": [] + }, + { + "text": "If put request is received without either header for a resource that already exists", + "children": [] + } + ] + } + ] + }, + { + "text": "Error Codes Requirements", + "children": [ + { + "text": "An LRS rejects with error code 400 Bad Request any request to an Resource which uses a parameter not recognized by the LRS", + "children": [] + }, + { + "text": "An LRS rejects with error code 400 Bad Request any request to an Resource which uses a parameter with differing case", + "children": [ + { + "text": "should fail on PUT statement when not using \"statementId\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"statementId\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"voidedStatementId\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"agent\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"verb\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"activity\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"registration\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"related_activities\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"related_agents\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"since\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"until\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"limit\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"format\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"attachments\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"ascending\"", + "children": [] + } + ] + }, + { + "text": "An LRS does not process any batch of Statements in which one or more Statements is rejected and if necessary, restores the LRS to the state in which it was before the batch began processing", + "children": [ + { + "text": "should not persist any statements on a single failure", + "children": [] + } + ] + } + ] + }, + { + "text": "Versioning Requirements", + "children": [ + { + "text": "An LRS sends a header response with \"X-Experience-API-Version\" as the name and the latest patch version after \"1.0.0\" as the value", + "children": [] + }, + { + "text": "An LRS will not modify Statements based on a \"version\" before \"1.0.1\"", + "children": [ + { + "text": "should not convert newer version format to prior version format", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request which does not use a \"X-Experience-API-Version\" header name to any Resource except the About Resource", + "children": [ + { + "text": "should pass when About GET without header \"X-Experience-API-Version\"", + "children": [] + }, + { + "text": "should fail when Statement GET without header \"X-Experience-API-Version\"", + "children": [] + }, + { + "text": "should fail when Statement POST without header \"X-Experience-API-Version\"", + "children": [] + }, + { + "text": "should fail when Statement PUT without header \"X-Experience-API-Version\"", + "children": [] + } + ] + } + ] + }, + { + "text": "Authentication Requirements", + "children": [ + { + "text": "An LRS must support HTTP Basic Authentication", + "children": [] + }, + { + "text": "An LRS rejects a Statement of bad authorization, either authentication needed or failed credentials, with error code 401 Unauthorized", + "children": [ + { + "text": "fails when given a random name pass pair", + "children": [] + }, + { + "text": "fails with a malformed header", + "children": [] + } + ] + } + ] + } + ] + } + }, + "2.0.0": { + "conformanceTestCount": 1442, + "tests": { + "text": "", + "children": [ + { + "text": "Content Type Requirements", + "children": [ + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and for any part except the first does not have a Header named \"Content-Transfer-Encoding\" with a value of \"binary\"", + "children": [] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request which uses Attachments and does not have a \"Content-Type\" header with value \"application/json\" or \"multipart/mixed\"", + "children": [ + { + "text": "should succeed when attachment uses \"fileUrl\" and request content-type is \"application/json\"", + "children": [] + }, + { + "text": "should succeed when attachment uses \"fileUrl\" and request content-type is \"application/json\"", + "children": [] + }, + { + "text": "should fail when attachment uses \"fileUrl\" and request content-type is \"multipart/form-data\"", + "children": [] + }, + { + "text": "should succeed when attachment is raw data and request content-type is \"multipart/mixed\"", + "children": [] + }, + { + "text": "should fail when attachment is raw data and request content-type is \"multipart/form-data\"", + "children": [] + }, + { + "text": "should succeed when attachment uses \"fileUrl\" and request content-type is \"multipart/mixed\"", + "children": [] + }, + { + "text": "should succeed when no attachments are included, but request content-type is \"multipart/mixed\"", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which has excess multi-part sections that are not attachments.", + "children": [ + { + "text": "should fail when passing statement attachments with excess multipart sections", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content-Type\" header with value \"application/json\", and has a discrepancy in the number of Attachments vs. the number of fileURL members", + "children": [ + { + "text": "should fail when passing statement attachments and missing attachment\"s binary", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and does not have a body header named \"boundary\"", + "children": [ + { + "text": "should fail if boundary not provided in body", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and does not have a Boundary before each \"Content-Type\" header", + "children": [ + { + "text": "should fail if boundary not provided in body", + "children": [] + }, + { + "text": "should fail if boundary not provided in header", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and does not the first document part with a \"Content-Type\" header with a value of \"application/json\"", + "children": [ + { + "text": "should fail when attachment is raw data and first part content type is not \"application/json\"", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and does not have all of the Statements in the first document part", + "children": [ + { + "text": "should fail when statements separated into multiple parts", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a \"Content Type\" header with value \"multipart/mixed\", and for any part except the first does not have a Header named \"X-Experience-API-Hash\" with a value of one of those found in a \"sha2\" property of a Statement in the first part of this document", + "children": [ + { + "text": "should fail when attachments missing header \"X-Experience-API-Hash\"", + "children": [] + }, + { + "text": "should fail when attachments header \"X-Experience-API-Hash\" does not match \"sha2\"", + "children": [] + } + ] + } + ] + }, + { + "text": "", + "children": [ + { + "text": "xAPI uses HTTP 1.1 entity tags (ETags) to implement optimistic concurrency control in the following resources, where PUT, POST or DELETE are allowed to overwrite or remove existing data.", + "children": [ + { + "text": "Concurrency for the Activity State Resource.", + "children": [ + { + "text": "An LRS responding to a GET request SHALL add an ETag HTTP header to the response.", + "children": [] + }, + { + "text": "When responding to a GET Request the Etag header must be enclosed in quotes", + "children": [] + }, + { + "text": "When responding to a PUT, POST, or DELETE request, must handle the If-Match header as described in RFC 2616, HTTP/1.1 if it contains an ETag", + "children": [ + { + "text": "Properly handles PUT requests with If-Match", + "children": [ + { + "text": "Should reject a PUT request with a 412 Precondition Failed when using an incorrect ETag", + "children": [] + }, + { + "text": "Should not have modified the document for PUT requests with an incorrect ETag", + "children": [] + }, + { + "text": "Should accept a PUT request with a correct ETag", + "children": [] + }, + { + "text": "Should have modified the document for PUT requests with a correct ETag", + "children": [] + } + ] + }, + { + "text": "Properly handles POST requests with If-Match", + "children": [ + { + "text": "Should reject a POST request with a 412 Precondition Failed when using an incorrect ETag", + "children": [] + }, + { + "text": "Should not have modified the document for POST requests with an incorrect ETag", + "children": [] + }, + { + "text": "Should accept a POST request with a correct ETag", + "children": [] + }, + { + "text": "Should have modified the document for POST requests with a correct ETag", + "children": [] + } + ] + }, + { + "text": "Properly handles DELETE requests with If-Match", + "children": [ + { + "text": "Should reject a DELETE request with a 412 Precondition Failed when using an incorrect ETag", + "children": [] + }, + { + "text": "Should not have modified the document for DELETE requests with an incorrect ETag", + "children": [] + }, + { + "text": "Should accept a DELETE request with a correct ETag", + "children": [] + } + ] + } + ] + }, + { + "text": "If a PUT request is received without either header for a resource that already exists", + "children": [] + } + ] + }, + { + "text": "Concurrency for the Activity Profile Resource.", + "children": [ + { + "text": "An LRS responding to a GET request SHALL add an ETag HTTP header to the response.", + "children": [] + }, + { + "text": "When responding to a GET Request the Etag header must be enclosed in quotes", + "children": [] + }, + { + "text": "When responding to a PUT, POST, or DELETE request, must handle the If-Match header as described in RFC 2616, HTTP/1.1 if it contains an ETag", + "children": [ + { + "text": "Properly handles PUT requests with If-Match", + "children": [ + { + "text": "Should reject a PUT request with a 412 Precondition Failed when using an incorrect ETag", + "children": [] + }, + { + "text": "Should not have modified the document for PUT requests with an incorrect ETag", + "children": [] + }, + { + "text": "Should accept a PUT request with a correct ETag", + "children": [] + }, + { + "text": "Should have modified the document for PUT requests with a correct ETag", + "children": [] + } + ] + }, + { + "text": "Properly handles POST requests with If-Match", + "children": [ + { + "text": "Should reject a POST request with a 412 Precondition Failed when using an incorrect ETag", + "children": [] + }, + { + "text": "Should not have modified the document for POST requests with an incorrect ETag", + "children": [] + }, + { + "text": "Should accept a POST request with a correct ETag", + "children": [] + }, + { + "text": "Should have modified the document for POST requests with a correct ETag", + "children": [] + } + ] + }, + { + "text": "Properly handles DELETE requests with If-Match", + "children": [ + { + "text": "Should reject a DELETE request with a 412 Precondition Failed when using an incorrect ETag", + "children": [] + }, + { + "text": "Should not have modified the document for DELETE requests with an incorrect ETag", + "children": [] + }, + { + "text": "Should accept a DELETE request with a correct ETag", + "children": [] + } + ] + } + ] + }, + { + "text": "If a PUT request is received without either header for a resource that already exists", + "children": [] + } + ] + }, + { + "text": "Concurrency for the Agents Profile Resource.", + "children": [ + { + "text": "An LRS responding to a GET request SHALL add an ETag HTTP header to the response.", + "children": [] + }, + { + "text": "When responding to a GET Request the Etag header must be enclosed in quotes", + "children": [] + }, + { + "text": "When responding to a PUT, POST, or DELETE request, must handle the If-Match header as described in RFC 2616, HTTP/1.1 if it contains an ETag", + "children": [ + { + "text": "Properly handles PUT requests with If-Match", + "children": [ + { + "text": "Should reject a PUT request with a 412 Precondition Failed when using an incorrect ETag", + "children": [] + }, + { + "text": "Should not have modified the document for PUT requests with an incorrect ETag", + "children": [] + }, + { + "text": "Should accept a PUT request with a correct ETag", + "children": [] + }, + { + "text": "Should have modified the document for PUT requests with a correct ETag", + "children": [] + } + ] + }, + { + "text": "Properly handles POST requests with If-Match", + "children": [ + { + "text": "Should reject a POST request with a 412 Precondition Failed when using an incorrect ETag", + "children": [] + }, + { + "text": "Should not have modified the document for POST requests with an incorrect ETag", + "children": [] + }, + { + "text": "Should accept a POST request with a correct ETag", + "children": [] + }, + { + "text": "Should have modified the document for POST requests with a correct ETag", + "children": [] + } + ] + }, + { + "text": "Properly handles DELETE requests with If-Match", + "children": [ + { + "text": "Should reject a DELETE request with a 412 Precondition Failed when using an incorrect ETag", + "children": [] + }, + { + "text": "Should not have modified the document for DELETE requests with an incorrect ETag", + "children": [] + }, + { + "text": "Should accept a DELETE request with a correct ETag", + "children": [] + } + ] + } + ] + }, + { + "text": "If a PUT request is received without either header for a resource that already exists", + "children": [] + } + ] + } + ] + } + ] + }, + { + "text": "Statement Resource Requirements", + "children": [ + { + "text": "An LRS has a Statement Resource with endpoint \"base IRI\"+\"/statements\"", + "children": [ + { + "text": "should allow \"/statements\" POST", + "children": [] + }, + { + "text": "should allow \"/statements\" PUT", + "children": [] + }, + { + "text": "should allow \"/statements\" GET", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful PUT request returns code 204 No Content", + "children": [ + { + "text": "should persist statement and return status 204", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource accepts PUT requests only if it contains a \"statementId\" parameter", + "children": [ + { + "text": "should persist statement using \"statementId\" parameter", + "children": [] + }, + { + "text": "should fail without using \"statementId\" parameter", + "children": [] + } + ] + }, + { + "text": "An LRS cannot modify a Statement, state, or Object in the event it receives a Statement with statementID equal to a Statement in the LRS already.", + "children": [ + { + "text": "should not update statement with matching \"statementId\" on PUT", + "children": [] + }, + { + "text": "should not update statement with matching \"statementId\" on POST", + "children": [] + }, + { + "text": "should reject a batch of two or more statements where the same ID is used more than once.", + "children": [] + }, + { + "text": "should include a Last-Modified header which matches the \"stored\" Timestamp of the statement.", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource accepts POST requests", + "children": [ + { + "text": "should persist statement using \"POST\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful POST request returns code 200 OK and all Statement UUIDs within the POST **Implicit**", + "children": [ + { + "text": "should persist statement using \"POST\" and return array of IDs", + "children": [] + } + ] + }, + { + "text": "LRS's Statement Resource accepts GET requests", + "children": [ + { + "text": "should return using GET", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful GET request with a \"statementId\" parameter, returns code 200 OK and a single Statement with the corresponding \"id\".", + "children": [ + { + "text": "should retrieve statement using \"statementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful GET request with a \"voidedStatementId\" parameter, returns code 200 OK and a single Statement with the corresponding \"id\".", + "children": [ + { + "text": "should return a voided statement when using GET \"voidedStatementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a successful GET request with neither a \"statementId\" nor a \"voidedStatementId\" parameter, returns code 200 OK and a StatementResult Object.", + "children": [ + { + "text": "should return StatementResult using GET without \"statementId\" or \"voidedStatementId\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"agent\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"verb\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"activity\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"registration\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"related_activities\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"related_agents\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"since\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"until\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"limit\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"ascending\"", + "children": [] + }, + { + "text": "should return StatementResult using GET with \"format\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"statementId\" as a parameter", + "children": [ + { + "text": "should process using GET with \"statementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"voidedStatementId\" as a parameter", + "children": [ + { + "text": "should process using GET with \"voidedStatementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"agent\" as a parameter", + "children": [ + { + "text": "should process using GET with \"agent\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"verb\" as a parameter", + "children": [ + { + "text": "should process using GET with \"verb\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"activity\" as a parameter", + "children": [ + { + "text": "should process using GET with \"activity\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"registration\" as a parameter", + "children": [ + { + "text": "should process using GET with \"registration\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"related_activities\" as a parameter", + "children": [ + { + "text": "should process using GET with \"related_activities\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"related_agents\" as a parameter", + "children": [ + { + "text": "should process using GET with \"related_agents\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"since\" as a parameter", + "children": [ + { + "text": "should process using GET with \"since\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"until\" as a parameter", + "children": [ + { + "text": "should process using GET with \"until\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"limit\" as a parameter", + "children": [ + { + "text": "should process using GET with \"limit\"", + "children": [] + } + ] + }, + { + "text": "If the \"Accept-Language\" header is present as part of the GET request to the Statement API and the \"format\" parameter is set to \"canonical\", the LRS MUST apply this data to choose the matching language in the response.", + "children": [ + { + "text": "should apply this data to choose the matching language in the response", + "children": [] + }, + { + "text": "should NOT apply this data to choose the matching language in the response when format is not set ", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"format\" as a parameter", + "children": [ + { + "text": "should process using GET with \"format\" absent", + "children": [] + }, + { + "text": "should process using GET with \"format\" canonical", + "children": [] + }, + { + "text": "should process using GET with \"format\" exact", + "children": [] + }, + { + "text": "should process using GET with \"format\" ids", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"attachments\" as a parameter", + "children": [ + { + "text": "should return multipart response format StatementResult using GET with \"attachments\" parameter as true", + "children": [] + }, + { + "text": "should not return multipart response format using GET with \"attachments\" parameter as false", + "children": [] + }, + { + "text": "should process using GET with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRSs Statement Resource, upon receiving a GET request, MUST have a \"Content-Type\" header", + "children": [ + { + "text": "should contain the content-type header", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource can process a GET request with \"ascending\" as a parameter", + "children": [ + { + "text": "should process using GET with \"ascending\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource rejects with error code 400 a GET request with both \"statementId\" and anything other than \"attachments\" or \"format\" as parameters", + "children": [ + { + "text": "should fail when using \"statementId\" with \"agent\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"verb\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"activity\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"registration\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"related_activities\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"related_agents\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"since\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"until\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"limit\"", + "children": [] + }, + { + "text": "should fail when using \"statementId\" with \"ascending\"", + "children": [] + }, + { + "text": "should pass when using \"statementId\" with \"format\"", + "children": [] + }, + { + "text": "should pass when using \"statementId\" with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource rejects with error code 400 a GET request with both \"voidedStatementId\" and anything other than \"attachments\" or \"format\" as parameters", + "children": [ + { + "text": "should fail when using \"voidedStatementId\" with \"agent\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"verb\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"activity\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"registration\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"related_activities\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"related_agents\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"since\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"until\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"limit\"", + "children": [] + }, + { + "text": "should fail when using \"voidedStatementId\" with \"ascending\"", + "children": [] + }, + { + "text": "should pass when using \"voidedStatementId\" with \"format\"", + "children": [] + }, + { + "text": "should pass when using \"voidedStatementId\" with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "The LRS will NOT reject a GET request which returns an empty \"statements\" property", + "children": [ + { + "text": "should return empty array list", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource upon processing a GET request, returns a header with name \"X-Experience-API-Consistent-Through\" regardless of the code returned.", + "children": [ + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" misusing GET", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"agent\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"verb\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"activity\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"registration\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"related_activities\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"related_agents\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"since\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"until\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"limit\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"ascending\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"format\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRS's \"X-Experience-API-Consistent-Through\" header is an ISO 8601 combined date and time", + "children": [ + { + "text": "should return valid \"X-Experience-API-Consistent-Through\" using GET", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"agent\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"verb\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"activity\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"registration\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"related_activities\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"related_agents\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"since\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"until\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"limit\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"ascending\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"format\"", + "children": [] + }, + { + "text": "should return \"X-Experience-API-Consistent-Through\" using GET with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRSs Statement Resource does not return attachment data and only returns application/json if the \"attachment\" parameter set to \"false\"", + "children": [ + { + "text": "should NOT return the attachment if \"attachments\" is missing", + "children": [] + }, + { + "text": "should NOT return the attachment if \"attachments\" is false", + "children": [] + }, + { + "text": "should return the attachment when \"attachment\" is true", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource, upon processing a successful GET request, can only return a Voided Statement if that Statement is specified in the voidedStatementId parameter of that request", + "children": [ + { + "text": "should not return a voided statement if using GET \"statementId\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource, upon processing a successful GET request wishing to return a Voided Statement still returns Statements which target it", + "children": [ + { + "text": "should only return statements stored after designated \"since\" timestamp when using \"since\" parameter", + "children": [] + }, + { + "text": "should only return statements stored at or before designated \"before\" timestamp when using \"until\" parameter", + "children": [] + }, + { + "text": "should return the number of statements listed in \"limit\" parameter", + "children": [] + }, + { + "text": "should return StatementRef and voiding statement when not using \"since\", \"until\", \"limit\"", + "children": [] + } + ] + }, + { + "text": "The Statements within the \"statements\" property will correspond to the filtering criterion sent in with the GET request", + "children": [ + { + "text": "should return StatementResult with statements as array using GET with \"agent\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"verb\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"activity\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"registration\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"related_activities\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"related_agents\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"since\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"until\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"limit\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"ascending\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"format\"", + "children": [] + }, + { + "text": "should return StatementResult with statements as array using GET with \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRS's Statement Resource rejects with error code 400 a GET request with additional properties than extensions in the locations where extensions are allowed", + "children": [ + { + "text": "should fail when using property not defined in specification", + "children": [] + } + ] + }, + { + "text": "The LRS shall set the \"timestamp\" property to the value of the \"stored\" property if not provided.", + "children": [ + { + "text": "should set timestamp property to equal \"stored\" value if retrieved statement does not have its own timestamp", + "children": [] + } + ] + }, + { + "text": "The LRS shall not reject a timestamp for having a greater value than the current time, within an acceptable margin of error", + "children": [ + { + "text": "accepts statements with greater value than current time", + "children": [] + } + ] + } + ] + }, + { + "text": "State Resource Requirements", + "children": [ + { + "text": "An LRS has a State Resource with endpoint \"base IRI\"+\"/activities/state\"", + "children": [] + }, + { + "text": "An LRS's State Resource accepts PUT requests", + "children": [] + }, + { + "text": "An LRS's State Resource accepts POST requests", + "children": [] + }, + { + "text": "An LRS's State Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's State Resource accepts DELETE requests", + "children": [] + }, + { + "text": "An LRS's State Resource upon processing a successful GET request with a valid \"stateId\" as a parameter returns the document satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's State Resource upon processing a successful DELETE request with a valid \"stateId\" as a parameter deletes the document satisfying the requirements of the DELETE and returns code 204 No Content", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a PUT request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a POST request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a GET request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a DELETE request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a PUT request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a PUT request with \"agent\" as a parameter if it is not in JSON format with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a POST request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a GET request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a DELETE request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource can process a PUT request with \"registration\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource, rejects a POST request if the document is found and either document's type is not \"application/json\" with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document", + "children": [] + }, + { + "text": "An LRS's State Resource performs a Document Merge if a document is found and both it and the document in the POST request have type \"application/json\"", + "children": [] + }, + { + "text": "An LRS must reject with 400 Bad Request a POST request to the State Resource which contains name/value pairs with invalid JSON and the Content-Type header is 'application/json'", + "children": [] + }, + { + "text": "An LRS's State Resource can process a POST request with \"registration\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource can process a GET request with \"registration\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource can process a DELETE request with \"registration\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a PUT request without \"stateId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a POST request without \"stateId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource can process a GET request with \"stateId\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource can process a GET request with \"since\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a GET request with \"since\" as a parameter if it is not a \"TimeStamp\", with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's State Resource can process a DELETE request with \"stateId\" as a parameter", + "children": [] + }, + { + "text": "An LRS's State Resource upon processing a successful GET request without \"stateId\" as a parameter returns an array of ids of state data documents satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's returned array of ids from a successful GET request to the State Resource all refer to documents stored after the TimeStamp in the \"since\" parameter of the GET request", + "children": [] + }, + { + "text": "An LRS's State Resource upon processing a successful DELETE request without \"stateId\" as a parameter deletes documents satisfying the requirements of the DELETE and code 204 No Content", + "children": [] + }, + { + "text": "An LRS's State Resource rejects a POST request with \"agent\" as a parameter if it is not in JSON format with error code 400 Bad Request", + "children": [ + { + "text": "Should reject POST State with agent invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a GET request with \"agent\" as a parameter if it is not in JSON format with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a DELETE request with \"agent\" as a parameter if it is not in JSON format with error code 400 Bad Request", + "children": [ + { + "text": "Should reject DELETE with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a PUT request with \"registration\" as a parameter if it is not a UUID with error code 400 Bad Request", + "children": [ + { + "text": "Should reject PUT with \"registration\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRSs State Resource, rejects a POST request if the document is found and either document is not a valid JSON Object", + "children": [ + { + "text": "If the document being posted to the State Resource does not have a Content-Type of application/json and the existing document does, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the existing document does not have a Content-Type of application/json but the document being posted to the State Resource does the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the document being posted to the State Resource has a content type of Content-Type of application/json but cannot be parsed as a JSON Object, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a POST request with \"registration\" as a parameter if it is not a UUID with error code 400 Bad Request", + "children": [ + { + "text": "Should reject POST with \"registration\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a GET request with \"registration\" as a parameter if it is not a UUID with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"registration\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's State Resource rejects a DELETE request with \"registration\" as a parameter if it is not a UUID with error code 400 Bad Request", + "children": [ + { + "text": "Should reject DELETE with \"registration\" with invalid value", + "children": [] + } + ] + }, + { + "text": "The LRS shall include a Last-Modified header indicating when the document was last modified.", + "children": [ + { + "text": "Returns a Last-Modified header at all", + "children": [] + }, + { + "text": "Updates the Last-Modified value when the corresponding document is updated.", + "children": [] + } + ] + } + ] + }, + { + "text": "Activities Resource Requirements", + "children": [ + { + "text": "An LRS has an Activities Resource with endpoint \"base IRI\" + /activities\"", + "children": [] + }, + { + "text": "An LRS's Activities Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's Activities Resource upon processing a successful GET request returns the complete Activity Object", + "children": [] + }, + { + "text": "An LRS's Activities Resource rejects a GET request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activities Resource rejects a GET request with \"activityId\" as a parameter if it is not type \"String\" with error code 400 Bad Request", + "children": [] + }, + { + "text": "The Activity Object must contain all available information about an activity from any statements who target the same \"activityId\". For example, LRS accepts two statements each with a different language description of an activity using the exact same \"activityId\". The LRS must return both language descriptions when a GET request is made to the Activities endpoint for that \"activityId\"", + "children": [] + }, + { + "text": "If an LRS does not have a canonical definition of the Activity to return, the LRS shall still return an Activity Object when queried.", + "children": [] + } + ] + }, + { + "text": "Agent Profile Resource Requirements", + "children": [ + { + "text": "An LRS's Agent Profile Resource upon processing a successful GET request with a valid \"profileId\" as a parameter returns the document satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a PUT request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a POST request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a POST request with \"agent\" as a parameter if it is not an Agent Object with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a DELETE request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a PUT request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a POST request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a DELETE request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource upon processing a successful GET request without \"profileId\" as a parameter returns an array of ids of agent profile documents satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource rejects a GET request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource can process a GET request with \"since\" as a parameter", + "children": [] + }, + { + "text": "An LRS's returned array of ids from a successful GET request to the Agent Profile Resource all refer to documents stored after the TimeStamp in the \"since\" parameter of the GET request if such a parameter was present", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource performs a Document Merge if a document is found and both it and the document in the POST request have type \"application/json\"", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource, rejects a POST request if the document is found and either document is not a valid JSON Object", + "children": [] + }, + { + "text": "An LRS must reject with 400 Bad Request a POST request to the Activitiy Profile Resource which contains name/value pairs with invalid JSON and the Content-Type header is 'application/json'", + "children": [] + }, + { + "text": "An LRS has an Agent Profile Resource with endpoint \"base IRI\"+\"/agents/profile\"", + "children": [ + { + "text": "An LRS's Agent Profile Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource upon processing a successful PUT request returns code 204 No Content", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource upon processing a successful POST request returns code 204 No Content", + "children": [] + }, + { + "text": "An LRS's Agent Profile Resource upon processing a successful DELETE request deletes the associated profile and returns code 204 No Content", + "children": [] + } + ] + }, + { + "text": "An LRS's Agent Profile Resource rejects a PUT request with \"agent\" as a parameter if it is not an Agent Object with error code 400 Bad Request", + "children": [ + { + "text": "Should reject PUT with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's Agent Profile Resource rejects a DELETE request with \"agent\" as a parameter if it is not an Agent Object with error code 400 Bad Request", + "children": [ + { + "text": "Should reject DELETE with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's Agent Profile Resource rejects a GET request with \"agent\" as a parameter if it is a valid, in structure, Agent with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"agent\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's Agent Profile Resource rejects a GET request with \"since\" as a parameter if it is not a \"TimeStamp\", with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"since\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRSs Agent Profile Resource, rejects a POST request if the document is found and either documents type is not \"application/json\" with error code 400 Bad Request", + "children": [ + { + "text": "If the document being posted to the Agent Profile Resource does not have a Content-Type of application/json and the existing document does, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the existing document does not have a Content-Type of application/json but the document being posted to the Agent Profile Resource does the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the document being posted to the Agent Profile Resource has a content type of Content-Type of application/json but cannot be parsed as a JSON Object, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + } + ] + }, + { + "text": "The LRS shall include a Last-Modified header indicating when the document was last modified.", + "children": [ + { + "text": "Returns a Last-Modified header at all", + "children": [] + }, + { + "text": "Updates the Last-Modified value when the corresponding document is updated.", + "children": [] + } + ] + } + ] + }, + { + "text": "Activity Profile Resource Requirements", + "children": [ + { + "text": "An LRS has an Activity Profile Resource with endpoint \"base IRI\"+\"/activities/profile\"", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource accepts POST requests", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource accepts DELETE requests", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource upon processing a successful GET request with a valid \"profileId\" as a parameter returns the document satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a PUT request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a POST request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a DELETE request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a GET request without \"activityId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a PUT request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a POST request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource rejects a DELETE request without \"profileId\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource upon processing a successful GET request without \"profileId\" as a parameter returns an array of ids of activity profile documents satisfying the requirements of the GET and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource can process a GET request with \"since\" as a parameter", + "children": [] + }, + { + "text": "An LRS's returned array of ids from a successful GET request to the Activity Profile Resource all refer to documents stored after the TimeStamp in the \"since\" parameter of the GET request if such a parameter was present", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource performs a Document Merge if a document is found and both it and the document in the POST request have type \"application/json\"", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource, rejects a POST request if the document is found and either document's type is not \"application/json\" with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS's must reject, with 400 Bad Request, a POST request to the Activity Profile Resource which contains name/value pairs with invalid JSON and the Content-Type header is \"application/json\"", + "children": [] + }, + { + "text": "An LRS's Activity Profile Resource accepts PUT requests", + "children": [ + { + "text": "passes with 204 no content", + "children": [] + } + ] + }, + { + "text": "An LRS's Activity Profile Resource rejects a GET request with \"since\" as a parameter if it is not a \"TimeStamp\", with error code 400 Bad Request", + "children": [ + { + "text": "Should reject GET with \"since\" with invalid value", + "children": [] + } + ] + }, + { + "text": "An LRS's Activity Profile Resource, rejects a POST request if the document is found and either document is not a valid JSON Object", + "children": [ + { + "text": "If the document being posted to the Activity Profile Resource does not have a Content-Type of application/json and the existing document does, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the existing document does not have a Content-Type of application/json but the document being posted to the Activity Profile Resource does the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + }, + { + "text": "If the document being posted to the Activity Profile Resource has a content type of Content-Type of application/json but cannot be parsed as a JSON Object, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", + "children": [] + } + ] + }, + { + "text": "The LRS shall include a Last-Modified header indicating when the document was last modified.", + "children": [ + { + "text": "Returns a Last-Modified header at all", + "children": [] + }, + { + "text": "Updates the Last-Modified value when the corresponding document is updated.", + "children": [] + } + ] + } + ] + }, + { + "text": "", + "children": [ + { + "text": "IRIs", + "children": [ + { + "text": "When storing or comparing IRIs, LRSs shall handle them only by using one or more of the approaches described in 5.3.1 (Simple String Comparison) and 5.3.2 (Syntax-Based Normalization) of RFC 3987", + "children": [] + } + ] + }, + { + "text": "Duration", + "children": [ + { + "text": "On receiving a Duration with more than 0.01 second precision, the LRS shall not reject the request.", + "children": [] + }, + { + "text": "On receiving a Duration with more than 0.01 second precision, the LRS may truncate the duration to 0.01 second precision.", + "children": [] + }, + { + "text": "When comparing Durations (or Statements containing them), any precision beyond 0.01 second precision shall not be included in the comparison.", + "children": [] + } + ] + }, + { + "text": "Timestamps", + "children": [ + { + "text": "checks if the LRS converts timestamps to UTC", + "children": [] + } + ] + } + ] + }, + { + "text": "Formatting Requirements", + "children": [ + { + "text": "A Statement contains an \"actor\" property", + "children": [ + { + "text": "statement \"actor\" missing", + "children": [] + } + ] + }, + { + "text": "A Statement contains a \"verb\" property", + "children": [ + { + "text": "statement \"verb\" missing", + "children": [] + } + ] + }, + { + "text": "A Statement contains an \"object\" property", + "children": [ + { + "text": "statement \"object\" missing", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request any Statement having a property whose value is set to \"null\", except in an \"extensions\" property", + "children": [ + { + "text": "statement actor should fail on \"null\"", + "children": [] + }, + { + "text": "statement verb should fail on \"null\"", + "children": [] + }, + { + "text": "statement context should fail on \"null\"", + "children": [] + }, + { + "text": "statement object should fail on \"null\"", + "children": [] + }, + { + "text": "statement activity extensions can be empty", + "children": [] + }, + { + "text": "statement result extensions can be empty", + "children": [] + }, + { + "text": "statement context extensions can be empty", + "children": [] + }, + { + "text": "statement substatement activity extensions can be empty", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data type", + "children": [ + { + "text": "with strings where numbers are required", + "children": [] + }, + { + "text": "even if those strings contain numbers", + "children": [] + }, + { + "text": "with strings where booleans are required", + "children": [] + }, + { + "text": "even if those strings contain booleans", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement which uses any non-format-following key or value, including the empty string, where a string with a particular format, such as mailto IRI, UUID, or IRI, is required.", + "children": [ + { + "text": "statement \"id\" invalid numeric", + "children": [] + }, + { + "text": "statement \"id\" invalid object", + "children": [] + }, + { + "text": "statement \"id\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement \"id\" invalid UUID with non A-F", + "children": [] + }, + { + "text": "statement actor \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement actor \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement authority \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement authority \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement context instructor \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement context instructor \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement context team \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement as \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement as \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group\" account \"name\" property is string", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group\" account \"name\" property is string", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement where the case of a key does not match the case specified in this specification.", + "children": [ + { + "text": "should fail when not using \"id\"", + "children": [] + }, + { + "text": "should fail when not using \"actor\"", + "children": [] + }, + { + "text": "should fail when not using \"verb\"", + "children": [] + }, + { + "text": "should fail when not using \"object\"", + "children": [] + }, + { + "text": "should fail when not using \"result\"", + "children": [] + }, + { + "text": "should fail when not using \"context\"", + "children": [] + }, + { + "text": "should fail when not using \"timestamp\"", + "children": [] + }, + { + "text": "should fail when not using \"stored\"", + "children": [] + }, + { + "text": "should fail when not using \"authority\"", + "children": [] + }, + { + "text": "should fail when not using \"version\"", + "children": [] + }, + { + "text": "should fail when not using \"attachments\"", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement where the case of a value restricted to enumerated values does not match an enumerated value given in this specification exactly.", + "children": [ + { + "text": "when interactionType is wrong case (\"true-faLse\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"choiCe\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"fill-iN\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"long-fiLl-in\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"matchIng\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"perfOrmance\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"seqUencing\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"liKert\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"nUmeric\")", + "children": [] + }, + { + "text": "when interactionType is wrong case (\"Other\")", + "children": [] + } + ] + }, + { + "text": "The LRS rejects with error code 400 Bad Request a token with does not validate as matching the RFC 5646 standard in the sequence of token lengths for language map keys.", + "children": [ + { + "text": "statement verb \"display\" should pass given de two letter language code only", + "children": [] + }, + { + "text": "statement verb \"display\" should fail given invalid language code", + "children": [] + }, + { + "text": "statement object \"name\" should pass given de-DE language-region code", + "children": [] + }, + { + "text": "statement object \"name\" should fail given invalid language code", + "children": [] + }, + { + "text": "statement object \"description\" should pass given zh-Hant language-script code", + "children": [] + }, + { + "text": "statement object \"description\" should fail given invalid language code", + "children": [] + }, + { + "text": "interaction components' description should pass given sr-Latn-RS language-script-region code", + "children": [] + }, + { + "text": "context.language should pass given three letter cmn language code", + "children": [] + }, + { + "text": "context.language should fail given invalid language code", + "children": [] + }, + { + "text": "statement attachment \"display\" should pass given es two letter language code only", + "children": [] + }, + { + "text": "statement attachment \"display\" should fail given invalid language code", + "children": [] + }, + { + "text": "statement attachment \"description\" should pass given es-MX language-UN region code", + "children": [] + }, + { + "text": "statement attachment \"description\" should fail given es-MX invalid language code", + "children": [] + }, + { + "text": "statement substatement verb \"display\" should pass given sr-Cyrl language-script code", + "children": [] + }, + { + "text": "statement substatement verb \"display\" should fail given invalid language code", + "children": [] + }, + { + "text": "statement substatement activity \"name\" should pass given zh-Hans-CN language-script-region code", + "children": [] + }, + { + "text": "statement substatement activity \"name\" should fail given zh-z-aaa-z-bbb-c-ccc invalid (two extensions with same single-letter prefix) language code", + "children": [] + }, + { + "text": "statement substatement activity \"description\" should pass given ase three letter language code", + "children": [] + }, + { + "text": "statement substatement activity \"description\" should fail given invalid language code", + "children": [] + }, + { + "text": "substatement interaction components' description should pass given ja two letter language code only", + "children": [] + }, + { + "text": "substatement context.language should pass given two letter fr-CA language-region code", + "children": [] + }, + { + "text": "substatement context.language should fail given invalid language code", + "children": [] + } + ] + }, + { + "text": "An LRS stores 32-bit floating point numbers with at least the precision of IEEE 754", + "children": [ + { + "text": "should pass and keep precision", + "children": [] + } + ] + }, + { + "text": "The LRS rejects with error code 400 Bad Request parameter values which do not validate to the same standards required for values of the same types in Statements", + "children": [ + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + }, + { + "text": "should reject when statementId value is invalid", + "children": [] + } + ] + }, + { + "text": "All Objects are well-created JSON Objects", + "children": [ + { + "text": "An LRS rejects a not well-created JSON Object", + "children": [] + }, + { + "text": "Statements Verify Templates", + "children": [ + { + "text": "should pass statement template", + "children": [] + } + ] + }, + { + "text": "Agents Verify Templates", + "children": [ + { + "text": "should pass statement actor template", + "children": [] + }, + { + "text": "should pass statement authority template", + "children": [] + }, + { + "text": "should pass statement context instructor template", + "children": [] + }, + { + "text": "should pass statement substatement as agent template", + "children": [] + }, + { + "text": "should pass statement substatement\"s agent template", + "children": [] + }, + { + "text": "should pass statement substatement\"s context instructor template", + "children": [] + } + ] + }, + { + "text": "Groups Verify Templates", + "children": [ + { + "text": "should pass statement actor template", + "children": [] + }, + { + "text": "should pass statement authority template", + "children": [] + }, + { + "text": "should pass statement context instructor template", + "children": [] + }, + { + "text": "should pass statement context team template", + "children": [] + }, + { + "text": "should pass statement substatement as group template", + "children": [] + }, + { + "text": "should pass statement substatement\"s group template", + "children": [] + }, + { + "text": "should pass statement substatement\"s context instructor template", + "children": [] + }, + { + "text": "should pass statement substatement\"s context team template", + "children": [] + } + ] + }, + { + "text": "A Group is defined by \"objectType\" of an \"actor\" property or \"object\" property with value \"Group\"", + "children": [ + { + "text": "statement actor \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement authority \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement context team \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement substatement as group \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s group \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" accepts \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"objectType\" accepts \"Group\"", + "children": [] + } + ] + }, + { + "text": "An Anonymous Group is defined by \"objectType\" of an \"actor\" or \"object\" with value \"Group\" and by none of \"mbox\", \"mbox_sha1sum\", \"openid\", or \"account\" being used", + "children": [ + { + "text": "statement actor does not require functional identifier", + "children": [] + }, + { + "text": "statement authority does not require functional identifier", + "children": [] + }, + { + "text": "statement context instructor does not require functional identifier", + "children": [] + }, + { + "text": "statement context team does not require functional identifier", + "children": [] + }, + { + "text": "statement substatement as group does not require functional identifier", + "children": [] + }, + { + "text": "statement substatement\"s group does not require functional identifier", + "children": [] + }, + { + "text": "statement substatement\"s context instructor does not require functional identifier", + "children": [] + }, + { + "text": "statement substatement\"s context team does not require functional identifier", + "children": [] + } + ] + }, + { + "text": "Verbs Verify Templates", + "children": [ + { + "text": "should pass statement verb template", + "children": [] + }, + { + "text": "should pass substatement verb template", + "children": [] + } + ] + }, + { + "text": "Objects Verify Templates", + "children": [ + { + "text": "should pass statement activity template", + "children": [] + }, + { + "text": "should pass statement substatement activity template", + "children": [] + }, + { + "text": "should pass statement agent template", + "children": [] + }, + { + "text": "should pass statement substatement agent template", + "children": [] + }, + { + "text": "should pass statement group template", + "children": [] + }, + { + "text": "should pass statement substatement group template", + "children": [] + }, + { + "text": "should pass statement StatementRef template", + "children": [] + }, + { + "text": "should pass statement substatement StatementRef template", + "children": [] + }, + { + "text": "should pass statement SubStatement template", + "children": [] + } + ] + }, + { + "text": "Activities Verify Templates", + "children": [ + { + "text": "should pass statement activity default template", + "children": [] + }, + { + "text": "should pass statement substatement activity default template", + "children": [] + }, + { + "text": "should pass statement activity choice template", + "children": [] + }, + { + "text": "should pass statement activity fill-in template", + "children": [] + }, + { + "text": "should pass statement activity numeric template", + "children": [] + }, + { + "text": "should pass statement activity likert template", + "children": [] + }, + { + "text": "should pass statement activity long-fill-in template", + "children": [] + }, + { + "text": "should pass statement activity matching template", + "children": [] + }, + { + "text": "should pass statement activity other template", + "children": [] + }, + { + "text": "should pass statement activity performance template", + "children": [] + }, + { + "text": "should pass statement activity sequencing template", + "children": [] + }, + { + "text": "should pass statement activity true-false template", + "children": [] + }, + { + "text": "should pass statement substatement activity choice template", + "children": [] + }, + { + "text": "should pass statement substatement activity likert template", + "children": [] + }, + { + "text": "should pass statement substatement activity matching template", + "children": [] + }, + { + "text": "should pass statement substatement activity performance template", + "children": [] + }, + { + "text": "should pass statement substatement activity sequencing template", + "children": [] + } + ] + }, + { + "text": "An Activity Definition uses the following properties: name, description, type, moreInfo, interactionType, or extensions", + "children": [ + { + "text": "statement activity \"definition\" missing all properties", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"name\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"description\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"type\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"moreInfo\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"extensions\"", + "children": [] + }, + { + "text": "statement activity \"definition\" contains \"interactionType\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" missing all properties", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"name\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"description\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"type\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"moreInfo\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"extensions\"", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" contains \"interactionType\"", + "children": [] + } + ] + }, + { + "text": "SubStatements Verify Templates", + "children": [ + { + "text": "should pass statement SubStatement template", + "children": [] + } + ] + }, + { + "text": "StatementRefs Verify Templates", + "children": [ + { + "text": "should pass statement StatementRef template", + "children": [] + }, + { + "text": "should pass substatement StatementRef template", + "children": [] + } + ] + }, + { + "text": "Results Verify Templates", + "children": [ + { + "text": "should pass statement result template", + "children": [] + }, + { + "text": "should pass substatement result template", + "children": [] + } + ] + }, + { + "text": "Contexts Verify Templates", + "children": [ + { + "text": "should pass statement context template", + "children": [] + }, + { + "text": "should pass substatement context template", + "children": [] + } + ] + }, + { + "text": "A ContextActivity is defined as a single Activity of the \"value\" of the \"contextActivities\" property", + "children": [ + { + "text": "statement context \"contextActivities parent\" value is activity", + "children": [] + }, + { + "text": "statement context \"contextActivities grouping\" value is activity", + "children": [] + }, + { + "text": "statement context \"contextActivities category\" value is activity", + "children": [] + }, + { + "text": "statement context \"contextActivities other\" value is activity", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities parent\" value is activity", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities grouping\" value is activity", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities category\" value is activity", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities other\" value is activity", + "children": [] + } + ] + }, + { + "text": "Languages Verify Templates", + "children": [ + { + "text": "should pass statement verb template", + "children": [] + }, + { + "text": "should pass statement object template", + "children": [] + }, + { + "text": "should pass statement attachment template", + "children": [] + }, + { + "text": "should pass statement substatement verb template", + "children": [] + }, + { + "text": "should pass statement substatement object template", + "children": [] + } + ] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI values without a scheme.", + "children": [ + { + "text": "should fail with bad verb id scheme", + "children": [] + }, + { + "text": "should fail with bad verb openid scheme", + "children": [] + }, + { + "text": "should fail with bad account homePage", + "children": [] + }, + { + "text": "should fail with bad object id", + "children": [] + }, + { + "text": "should fail with bad object type", + "children": [] + }, + { + "text": "should fail with bad object moreInfo", + "children": [] + }, + { + "text": "should fail with attachment bad usageType", + "children": [] + }, + { + "text": "should fail with bad attachment fileUrl", + "children": [] + }, + { + "text": "should fail with bad object definition extension", + "children": [] + }, + { + "text": "should fail with bad context extension", + "children": [] + }, + { + "text": "should fail with bad result extension", + "children": [] + } + ] + } + ] + }, + { + "text": "Statement Lifecycle Requirements", + "children": [ + { + "text": "A Voiding Statement is defined as a Statement whose \"verb\" property's \"id\" property's IRI ending with \"voided\"", + "children": [ + { + "text": "statement verb voided IRI ends with \"voided\" (WARNING: this applies \"Upon receiving a Statement that voids another, the LRS SHOULD NOT* reject the request on the grounds of the Object of that voiding Statement not being present\")", + "children": [] + } + ] + }, + { + "text": "A Voiding Statement's \"objectType\" field has a value of \"StatementRef\"", + "children": [ + { + "text": "statement verb voided uses substatement with \"StatementRef\"", + "children": [] + }, + { + "text": "statement verb voided does not use object \"StatementRef\"", + "children": [] + } + ] + }, + { + "text": "A Voided Statement is defined as a Statement that is not a Voiding Statement and is the Target of a Voiding Statement within the LRS", + "children": [ + { + "text": "Should return a voided statement when using GET \"voidedStatementId\"", + "children": [] + }, + { + "text": "Should return 404 when using GET with \"statementId\"", + "children": [] + } + ] + }, + { + "text": "A Voiding Statement cannot Target another Voiding Statement", + "children": [ + { + "text": "Should not void an already voided statement", + "children": [] + }, + { + "text": "Should not void a voiding statement", + "children": [] + } + ] + }, + { + "text": "An LRS SHALL NOT reject a voided statement because it cannot find the ID of the Object of that statement, nor does the LRS have to try to find it.", + "children": [ + { + "text": "Shall not reject a voided statement.", + "children": [] + } + ] + } + ] + }, + { + "text": "Id Property Requirements", + "children": [ + { + "text": "All UUID types follow requirements of RFC4122", + "children": [ + { + "text": "statement \"id\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement \"id\" invalid UUID with non A-F", + "children": [] + }, + { + "text": "statement object statementref \"id\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement object statementref \"id\" invalid UUID with non A-F", + "children": [] + }, + { + "text": "statement context \"registration\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement context \"registration\" invalid UUID with non A-F", + "children": [] + }, + { + "text": "statement context \"statement\" invalid UUID with too many digits", + "children": [] + }, + { + "text": "statement substatement context \"statement\" invalid UUID with non A-F", + "children": [] + } + ] + }, + { + "text": "All UUID types are in standard String form", + "children": [ + { + "text": "statement \"id\" invalid numeric", + "children": [] + }, + { + "text": "statement \"id\" invalid object", + "children": [] + }, + { + "text": "statement object statementref \"id\" invalid numeric", + "children": [] + }, + { + "text": "statement object statementref \"id\" invalid object", + "children": [] + }, + { + "text": "statement context \"registration\" invalid numeric", + "children": [] + }, + { + "text": "statement context \"registration\" invalid object", + "children": [] + }, + { + "text": "statement context \"statement\" invalid numeric", + "children": [] + }, + { + "text": "statement substatement context \"statement\" invalid object", + "children": [] + } + ] + }, + { + "text": "An LRS generates the \"id\" property of a Statement if none is provided", + "children": [ + { + "text": "should complete an empty id property", + "children": [] + } + ] + } + ] + }, + { + "text": "Actor Property Requirements", + "children": [ + { + "text": "An \"actor\" property's \"objectType\" property is either \"Agent\" or \"Group\"", + "children": [ + { + "text": "statement actor \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement actor \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement authority \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement authority \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement substatement as agent with \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement substatement as group with \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s actor \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement substatement\"s actor \"objectType\" should fail when not \"Group\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" should fail when not \"Agent\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" should fail when not \"Group\"", + "children": [] + } + ] + }, + { + "text": "An \"objectType\" property is a String", + "children": [ + { + "text": "statement actor \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement actor \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement authority \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement authority \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement substatement as agent with \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement as agent with \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement substatement\"s agent \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement\"s agent \"objectType\" should fail object", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" should fail object", + "children": [] + } + ] + }, + { + "text": "A \"name\" property is a String", + "children": [ + { + "text": "statement actor \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement actor \"name\" should fail object", + "children": [] + }, + { + "text": "statement authority \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement authority \"name\" should fail object", + "children": [] + }, + { + "text": "statement context instructor \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement context instructor \"name\" should fail object", + "children": [] + }, + { + "text": "statement substatement as agent with \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement as agent with \"name\" should fail object", + "children": [] + }, + { + "text": "statement substatement\"s agent \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement\"s agent \"name\" should fail object", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"name\" should fail numeric", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"name\" should fail object", + "children": [] + } + ] + }, + { + "text": "An \"actor\" property with \"objectType\" as \"Agent\" uses one of the following properties: \"mbox\", \"mbox_sha1sum\", \"openid\", \"account\"", + "children": [ + { + "text": "statement actor without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement actor \"account\" should pass", + "children": [] + }, + { + "text": "statement actor \"mbox\" should pass", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement actor \"openid\" should pass", + "children": [] + }, + { + "text": "statement authority without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement authority \"account\" should pass", + "children": [] + }, + { + "text": "statement authority \"mbox\" should pass", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement authority \"openid\" should pass", + "children": [] + }, + { + "text": "statement context instructor without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement context instructor \"account\" should pass", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" should pass", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement context instructor \"openid\" should pass", + "children": [] + }, + { + "text": "statement substatement as agent without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement substatement as agent \"account\" should pass", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox\" should pass", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement substatement as agent \"openid\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s agent without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s context instructor without \"account\", \"mbox\", \"mbox_sha1sum\", \"openid\" should fail", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" should pass", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" should pass", + "children": [] + } + ] + }, + { + "text": "An Agent is defined by \"objectType\" of an \"actor\" property or \"object\" property with value \"Agent\"", + "children": [ + { + "text": "statement actor does not require objectType", + "children": [] + }, + { + "text": "statement actor \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement authority \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement context instructor \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement substatement as agent \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"objectType\" accepts \"Agent\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"objectType\" accepts \"Agent\"", + "children": [] + } + ] + }, + { + "text": "An Agent does not use the \"mbox\" property if \"mbox_sha1sum\", \"openid\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Agent does not use the \"mbox_sha1sum\" property if \"mbox\", \"openid\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as agent \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Agent does not use the \"account\" property if \"mbox\", \"mbox_sha1sum\", or \"openid\" are used", + "children": [ + { + "text": "statement actor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as agent \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as agent \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as agent \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Agent does not use the \"openid\" property if \"mbox\", \"mbox_sha1sum\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as agent \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as agent \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as agent \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + } + ] + }, + { + "text": "An Anonymous Group uses the \"member\" property", + "children": [ + { + "text": "statement actor anonymous group missing member", + "children": [] + }, + { + "text": "statement authority anonymous group missing member", + "children": [] + }, + { + "text": "statement context instructor anonymous group missing member", + "children": [] + }, + { + "text": "statement context team anonymous group missing member", + "children": [] + }, + { + "text": "statement substatement as group anonymous group missing member", + "children": [] + }, + { + "text": "statement substatement\"s group anonymous group missing member", + "children": [] + }, + { + "text": "statement substatement\"s context instructor anonymous group missing member", + "children": [] + }, + { + "text": "statement substatement\"s context team anonymous group missing member", + "children": [] + } + ] + }, + { + "text": "The \"member\" property is an array of Objects following Agent requirements", + "children": [ + { + "text": "statement actor requires member type \"array\"", + "children": [] + }, + { + "text": "statement authority requires member type \"array\"", + "children": [] + }, + { + "text": "statement context instructor requires member type \"array\"", + "children": [] + }, + { + "text": "statement context team requires member type \"array\"", + "children": [] + }, + { + "text": "statement substatement as group requires member type \"array\"", + "children": [] + }, + { + "text": "statement substatement\"s group requires member type \"array\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor requires member type \"array\"", + "children": [] + }, + { + "text": "statement substatement\"s context team requires member type \"array\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group is defined by \"objectType\" of an \"actor\" or \"object\" with value \"Group\" and by one of \"mbox\", \"mbox_sha1sum\", \"openid\", or \"account\" being used", + "children": [ + { + "text": "statement actor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"account\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group uses one of the following properties: \"mbox\", \"mbox_sha1sum\", \"openid\", \"account\"", + "children": [ + { + "text": "statement actor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement actor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement context instructor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement context team identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s group identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor identified group accepts \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team identified group accepts \"account\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group does not use the \"mbox\" property if \"mbox_sha1sum\", \"openid\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor \"mbox\" cannot be used with \"openid", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context team \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context team \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group does not use the \"mbox_sha1sum\" property if \"mbox\", \"openid\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"mbox_sha1sum\" cannot be used with \"openid", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context team \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context team \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context team \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox_sha1sum\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox_sha1sum\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"mbox_sha1sum\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group does not use the \"openid\" property if \"mbox\", \"mbox_sha1sum\", or \"account\" are used", + "children": [ + { + "text": "statement actor \"openid\" cannot be used with \"account", + "children": [] + }, + { + "text": "statement actor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement context team \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context team \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement as group \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"openid\" cannot be used with \"account\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"openid\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"openid\" cannot be used with \"mbox_sha1sum\"", + "children": [] + } + ] + }, + { + "text": "An Identified Group does not use the \"account\" property if \"mbox\", \"mbox_sha1sum\", or \"openid\" are used", + "children": [ + { + "text": "statement actor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement actor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement actor \"account\" cannot be used with \"openid", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement authority \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context instructor \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement context team \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement context team \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement context team \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement as group \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement as group \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement as group \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s agent \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"account\" cannot be used with \"openid\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"account\" cannot be used with \"mbox\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"account\" cannot be used with \"mbox_sha1sum\"", + "children": [] + }, + { + "text": "statement substatement\"s context team \"account\" cannot be used with \"openid\"", + "children": [] + } + ] + }, + { + "text": "An \"mbox\" property has the form \"mailto:email address\" and is an IRI", + "children": [ + { + "text": "statement actor \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement actor \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement authority \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement authority \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement context instructor \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement context instructor \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement context team \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement as \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement as \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group mbox\" not IRI", + "children": [] + }, + { + "text": "statement actor \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement actor \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement authority \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement authority \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement context instructor \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement context instructor \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement context team \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement as \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement as \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group mbox\" not mailto:email address", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group mbox\" not mailto:email address", + "children": [] + } + ] + }, + { + "text": "An \"mbox_sha1sum\" property is a String", + "children": [ + { + "text": "statement actor \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement actor \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement authority \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement authority \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement context instructor \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement context instructor \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement context team \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement as \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement as \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group mbox_sha1sum\" not string", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group mbox_sha1sum\" not string", + "children": [] + } + ] + }, + { + "text": "An \"openid\" property is a URI", + "children": [ + { + "text": "statement actor \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement actor \"group openid\" not URI", + "children": [] + }, + { + "text": "statement authority \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement authority \"group openid\" not URI", + "children": [] + }, + { + "text": "statement context instructor \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement context instructor \"group openid\" not URI", + "children": [] + }, + { + "text": "statement context team \"group openid\" not URI", + "children": [] + }, + { + "text": "statement substatement as \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement substatement as \"group openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s \"group openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group openid\" not URI", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group openid\" not URI", + "children": [] + } + ] + }, + { + "text": "An Account Object is the \"account\" property of a Group or Agent", + "children": [ + { + "text": "statement actor \"agent account\" property exists", + "children": [] + }, + { + "text": "statement actor \"group account\" property exists", + "children": [] + }, + { + "text": "statement authority \"agent account\" property exists", + "children": [] + }, + { + "text": "statement authority \"group account\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"agent account\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"group account\" property exists", + "children": [] + }, + { + "text": "statement context team \"group account\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"agent account\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"group account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"agent account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"group account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group account\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group account\" property exists", + "children": [] + } + ] + }, + { + "text": "An Account Object uses the \"homePage\" property", + "children": [ + { + "text": "statement actor \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement actor \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement authority \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement authority \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement context team \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group\" account \"homePage\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group\" account \"homePage\" property exists", + "children": [] + } + ] + }, + { + "text": "An Account Object's \"homePage\" property is an IRL", + "children": [ + { + "text": "statement actor \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement actor \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement authority \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement authority \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement context instructor \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement context instructor \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement context team \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement as \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement as \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group\" account \"homePage property is IRL", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group\" account \"homePage property is IRL", + "children": [] + } + ] + }, + { + "text": "An Account Object uses the \"name\" property", + "children": [ + { + "text": "statement actor \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement actor \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement authority \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement authority \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement context instructor \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement context team \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement as \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"agent\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context instructor \"group\" account \"name\" property exists", + "children": [] + }, + { + "text": "statement substatement\"s context team \"group\" account \"name\" property exists", + "children": [] + } + ] + } + ] + }, + { + "text": "Verb Property Requirements", + "children": [ + { + "text": "A \"verb\" property contains an \"id\" property", + "children": [ + { + "text": "statement verb missing \"id\"", + "children": [] + }, + { + "text": "statement substatement verb missing \"id\"", + "children": [] + } + ] + }, + { + "text": "A \"verb\" property's \"id\" property is an IRI", + "children": [ + { + "text": "statement verb \"id\" not IRI", + "children": [] + }, + { + "text": "statement substatement verb \"id\" not IRI", + "children": [] + } + ] + }, + { + "text": "A \"verb\" property's \"display\" property is a Language Map", + "children": [ + { + "text": "statement verb \"display\" is numeric", + "children": [] + }, + { + "text": "statement verb \"display\" is string", + "children": [] + }, + { + "text": "statement substatement verb \"display\" is numeric", + "children": [] + }, + { + "text": "statement substatement verb \"display\" is string", + "children": [] + } + ] + } + ] + }, + { + "text": "Object Property Requirements", + "children": [ + { + "text": "An \"object\" property's \"objectType\" property is either \"Activity\", \"Agent\", \"Group\", \"SubStatement\", or \"StatementRef\"", + "children": [ + { + "text": "statement activity should fail on \"activity\"", + "children": [] + }, + { + "text": "statement substatement activity should fail on \"activity\"", + "children": [] + }, + { + "text": "statement agent template should fail on \"agent\"", + "children": [] + }, + { + "text": "statement substatement agent should fail on \"agent\"", + "children": [] + }, + { + "text": "statement group should fail on \"group\"", + "children": [] + }, + { + "text": "statement substatement group should fail on \"group\"", + "children": [] + }, + { + "text": "statement StatementRef should fail on \"statementref\"", + "children": [] + }, + { + "text": "statement substatement StatementRef should fail on \"statementref\"", + "children": [] + }, + { + "text": "statement SubStatement should fail on \"substatement\"", + "children": [] + } + ] + }, + { + "text": "An \"object\" property uses the \"id\" property exactly one time", + "children": [ + { + "text": "statement activity \"id\" not provided", + "children": [] + }, + { + "text": "statement substatement activity \"id\" not provided", + "children": [] + } + ] + }, + { + "text": "An \"object\" property's \"id\" property is an IRI", + "children": [ + { + "text": "statement activity \"id\" not IRI", + "children": [] + }, + { + "text": "statement activity \"id\" is IRI", + "children": [] + }, + { + "text": "statement substatement activity \"id\" not IRI", + "children": [] + }, + { + "text": "statement substatement activity \"id\" is IRI", + "children": [] + } + ] + }, + { + "text": "An Activity's \"definition\" property is an Object", + "children": [ + { + "text": "statement activity \"definition\" not object", + "children": [] + }, + { + "text": "statement substatement activity \"definition\" not object", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"name\" property is a Language Map", + "children": [ + { + "text": "statement object \"name\" language map is numeric", + "children": [] + }, + { + "text": "statement object \"name\" language map is string", + "children": [] + }, + { + "text": "statement substatement activity \"name\" language map is numeric", + "children": [] + }, + { + "text": "statement substatement activity \"name\" language map is string", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"description\" property is a Language Map", + "children": [ + { + "text": "statement object \"description\" language map is numeric", + "children": [] + }, + { + "text": "statement object \"description\" language map is string", + "children": [] + }, + { + "text": "statement substatement activity \"description\" language map is numeric", + "children": [] + }, + { + "text": "statement substatement activity \"description\" language map is string", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"type\" property is an IRI", + "children": [ + { + "text": "statement activity \"type\" not IRI", + "children": [] + }, + { + "text": "statement substatement activity \"type\" not IRI", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"moreinfo\" property is an IRL", + "children": [ + { + "text": "statement activity \"moreInfo\" not IRI", + "children": [] + }, + { + "text": "statement substatement activity \"moreInfo\" not IRI", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"interactionType\" property is a String with a value of either “true-false”, “choice”, “fill-in”, “long-fill-in”, “matching”, “performance”, “sequencing”, “likert”, “numeric” or “other”", + "children": [ + { + "text": "statement activity \"interactionType\" can be used with \"true-false\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"choice\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"fill-in\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"long-fill-in\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"matching\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"performance\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"sequencing\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"likert\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"numeric\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" can be used with \"other\"", + "children": [] + }, + { + "text": "statement activity \"interactionType\" fails with invalid iri", + "children": [] + }, + { + "text": "statement activity \"interactionType\" fails with invalid numeric", + "children": [] + }, + { + "text": "statement activity \"interactionType\" fails with invalid object", + "children": [] + }, + { + "text": "statement activity \"interactionType\" fails with invalid string", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"true-false\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"choice\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"fill-in\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"long-fill-in\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"matching\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"performance\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"sequencing\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"likert\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"numeric\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" can be used with \"other\"", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" fails with invalid iri", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" fails with invalid numeric", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" fails with invalid object", + "children": [] + }, + { + "text": "statement substatement activity \"interactionType\" fails with invalid string", + "children": [] + } + ] + }, + { + "text": "An Activity Definition's \"extension\" property is an Object", + "children": [ + { + "text": "statement activity \"extension\" invalid string", + "children": [] + }, + { + "text": "statement activity \"extension\" invalid iri", + "children": [] + }, + { + "text": "statement substatement activity \"extension\" invalid string", + "children": [] + }, + { + "text": "statement substatement activity \"extension\" invalid iri", + "children": [] + } + ] + }, + { + "text": "An Activity Definition uses the \"interactionType\" property if any of the correctResponsesPattern, choices, scale, source, target, or steps properties are used", + "children": [ + { + "text": "Activity Definition uses correctResponsesPattern without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses choices without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses fill-in without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses scale without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses long-fill-in without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses source without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses target without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses numeric without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses other without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses performance without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses sequencing without \"interactionType\" property", + "children": [] + }, + { + "text": "Activity Definition uses true-false without \"interactionType\" property", + "children": [] + } + ] + }, + { + "text": "Statements that use an Agent or Group as an Object MUST specify an \"objectType\" property.", + "children": [ + { + "text": "should fail when using agent as object and no objectType", + "children": [] + }, + { + "text": "should fail when using group as object and no objectType", + "children": [] + }, + { + "text": "substatement should fail when using agent as object and no objectType", + "children": [] + }, + { + "text": "substatement should fail when using group as object and no objectType", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement is defined by the \"objectType\" of an \"object\" with value \"SubStatement\"", + "children": [ + { + "text": "substatement invalid when not \"SubStatement\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement follows the requirements of all Statements", + "children": [ + { + "text": "substatement requires actor", + "children": [] + }, + { + "text": "substatement requires object", + "children": [] + }, + { + "text": "substatement requires verb", + "children": [] + }, + { + "text": "should pass substatement context", + "children": [] + }, + { + "text": "should pass substatement result", + "children": [] + }, + { + "text": "should pass substatement statementref", + "children": [] + }, + { + "text": "should pass substatement as agent", + "children": [] + }, + { + "text": "should pass substatement as group", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot have a Sub-Statement", + "children": [ + { + "text": "substatement invalid nested \"SubStatement\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot use the \"id\" property at the Statement level", + "children": [ + { + "text": "substatement invalid with property \"id\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot use the \"stored\" property", + "children": [ + { + "text": "substatement invalid with property \"stored\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot use the \"version\" property", + "children": [ + { + "text": "substatement invalid with property \"version\"", + "children": [] + } + ] + }, + { + "text": "A Sub-Statement cannot use the \"authority\" property", + "children": [ + { + "text": "substatement invalid with property \"authority\"", + "children": [] + } + ] + }, + { + "text": "A Statement Reference is defined by the \"objectType\" of an \"object\" with value \"StatementRef\"", + "children": [ + { + "text": "statementref invalid when not \"StatementRef\"", + "children": [] + }, + { + "text": "substatement statementref invalid when not \"StatementRef\"", + "children": [] + } + ] + }, + { + "text": "A Statement Reference contains an \"id\" property", + "children": [ + { + "text": "statementref invalid when missing \"id\"", + "children": [] + }, + { + "text": "substatement statementref invalid when missing \"id\"", + "children": [] + } + ] + }, + { + "text": "A Statement Reference's \"id\" property is a UUID", + "children": [ + { + "text": "statementref \"id\" not \"uuid\"", + "children": [] + }, + { + "text": "substatement statementref \"id\" not \"uuid\"", + "children": [] + } + ] + } + ] + }, + { + "text": "Result Property Requirements", + "children": [ + { + "text": "A \"success\" property is a Boolean", + "children": [ + { + "text": "statement result \"success\" property is string \"true\"", + "children": [] + }, + { + "text": "statement result \"success\" property is string \"false\"", + "children": [] + }, + { + "text": "statement substatement result \"success\" property is string \"true\"", + "children": [] + }, + { + "text": "statement substatement result \"success\" property is string \"false\"", + "children": [] + } + ] + }, + { + "text": "A \"completion\" property is a Boolean", + "children": [ + { + "text": "statement result \"completion\" property is string \"true\"", + "children": [] + }, + { + "text": "statement result \"completion\" property is string \"false\"", + "children": [] + }, + { + "text": "statement substatement result \"completion\" property is string \"true\"", + "children": [] + }, + { + "text": "statement substatement result \"completion\" property is string \"false\"", + "children": [] + } + ] + }, + { + "text": "A \"response\" property is a String", + "children": [ + { + "text": "statement result \"response\" property is numeric", + "children": [] + }, + { + "text": "statement result \"completion\" property is object", + "children": [] + }, + { + "text": "statement substatement result \"completion\" property is numeric", + "children": [] + }, + { + "text": "statement substatement result \"completion\" property is object", + "children": [] + } + ] + }, + { + "text": "A \"duration\" property is a formatted to ISO 8601", + "children": [ + { + "text": "statement result \"duration\" property is invalid", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid", + "children": [] + } + ] + }, + { + "text": "An \"extensions\" property is an Object", + "children": [ + { + "text": "statement result \"extensions\" property is numeric", + "children": [] + }, + { + "text": "statement result \"extensions\" property is string", + "children": [] + }, + { + "text": "statement substatement result \"extensions\" property is numeric", + "children": [] + }, + { + "text": "statement substatement result \"extensions\" property is string", + "children": [] + } + ] + }, + { + "text": "A \"score\" property is an Object", + "children": [ + { + "text": "statement result score numeric", + "children": [] + }, + { + "text": "statement result score string", + "children": [] + }, + { + "text": "statement substatement result score numeric", + "children": [] + }, + { + "text": "statement substatement result score string", + "children": [] + } + ] + }, + { + "text": "A \"score\" Object's \"scaled\" property is a decimal number between -1 and 1, inclusive.", + "children": [ + { + "text": "statement result \"scaled\" accepts decimal", + "children": [] + }, + { + "text": "statement substatement result \"scaled\" accepts decimal", + "children": [] + }, + { + "text": "statement result \"scaled\" should pass with value 1.0", + "children": [] + }, + { + "text": "statement substatement result \"scaled\" pass with value -1.0000", + "children": [] + }, + { + "text": "statement result \"scaled\" should reject with value 1.01", + "children": [] + }, + { + "text": "statement substatement result \"scaled\" reject with value -1.00001", + "children": [] + } + ] + }, + { + "text": "A \"score\" Object's \"raw\" property is a decimal number between min and max, if present and otherwise unrestricted, inclusive", + "children": [ + { + "text": "statement result \"raw\" accepts decimal", + "children": [] + }, + { + "text": "statement substatement result \"raw\" accepts decimal", + "children": [] + }, + { + "text": "statement result \"raw\" rejects raw greater than max", + "children": [] + }, + { + "text": "statement substatement result \"raw\" rejects raw greater than max", + "children": [] + }, + { + "text": "statement result \"raw\" rejects raw less than min", + "children": [] + }, + { + "text": "statement substatement result \"raw\" rejects raw less than min", + "children": [] + } + ] + }, + { + "text": "A \"score\" Object's \"min\" property is a decimal number less than the \"max\" property, if it is present.", + "children": [ + { + "text": "statement result \"min\" accepts decimal", + "children": [] + }, + { + "text": "statement substatement result \"min\" accepts decimal", + "children": [] + }, + { + "text": "statement result \"min\" rejects decimal number greater than \"max\"", + "children": [] + }, + { + "text": "statement substatement result \"min\" rejects decimal number greater than \"max\"", + "children": [] + } + ] + }, + { + "text": "A \"score\" Object's \"max\" property is a Decimal accurate to seven significant decimal figures", + "children": [ + { + "text": "statement result \"max\" accepts a decimal number more than the \"min\" property, if it is present.", + "children": [] + }, + { + "text": "statement substatement result \"max\" accepts a decimal number more than the \"min\" property, if it is present.", + "children": [] + }, + { + "text": "statement result \"max\" accepts a decimal number more than the \"min\" property, if it is present.", + "children": [] + }, + { + "text": "statement substatement result \"max\" accepts a decimal number more than the \"min\" property, if it is present.", + "children": [] + } + ] + } + ] + }, + { + "text": "Context Property Requirements", + "children": [ + { + "text": "A \"registration\" property is a UUID", + "children": [ + { + "text": "statement context \"registration\" is object", + "children": [] + }, + { + "text": "statement context \"registration\" is string", + "children": [] + }, + { + "text": "statement substatement context \"registration\" is object", + "children": [] + }, + { + "text": "statement substatement context \"registration\" is string", + "children": [] + } + ] + }, + { + "text": "An \"instructor\" property is an Agent", + "children": [ + { + "text": "statement context \"instructor\" is object", + "children": [] + }, + { + "text": "statement context \"instructor\" is string", + "children": [] + }, + { + "text": "statement substatement context \"instructor\" is object", + "children": [] + }, + { + "text": "statement substatement context \"instructor\" is string", + "children": [] + } + ] + }, + { + "text": "An \"team\" property is a Group", + "children": [ + { + "text": "statement context \"team\" is agent", + "children": [] + }, + { + "text": "statement context \"team\" is object", + "children": [] + }, + { + "text": "statement context \"team\" is string", + "children": [] + }, + { + "text": "statement substatement context \"team\" is agent", + "children": [] + }, + { + "text": "statement substatement context \"team\" is object", + "children": [] + }, + { + "text": "statement substatement context \"team\" is string", + "children": [] + } + ] + }, + { + "text": "A \"contextActivities\" property is an Object", + "children": [ + { + "text": "statement context \"contextActivities\" is string", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is string", + "children": [] + } + ] + }, + { + "text": "A \"revision\" property is a String", + "children": [ + { + "text": "statement context \"revision\" is numeric", + "children": [] + }, + { + "text": "statement context \"revision\" is object", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is numeric", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is object", + "children": [] + } + ] + }, + { + "text": "A Statement cannot contain both a \"revision\" property in its \"context\" property and have the value of the \"object\" property's \"objectType\" be anything but \"Activity\"", + "children": [ + { + "text": "statement context \"revision\" is invalid with object agent", + "children": [] + }, + { + "text": "statement context \"revision\" is invalid with object group", + "children": [] + }, + { + "text": "statement context \"revision\" is invalid with statementref", + "children": [] + }, + { + "text": "statement context \"revision\" is invalid with substatement", + "children": [] + }, + { + "text": "statement context \"revision\" is valid with no ObjectType", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is invalid with object agent", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is invalid with object group", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is invalid with statementref", + "children": [] + }, + { + "text": "statement substatement context \"revision\" is valid with no objectType", + "children": [] + } + ] + }, + { + "text": "A \"platform\" property is a String", + "children": [ + { + "text": "statement context \"platform\" is numeric", + "children": [] + }, + { + "text": "statement context \"platform\" is object", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is numeric", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is object", + "children": [] + } + ] + }, + { + "text": "A Statement cannot contain both a \"platform\" property in its \"context\" property and have the value of the \"object\" property's \"objectType\" be anything but \"Activity\"", + "children": [ + { + "text": "statement context \"platform\" is invalid with object agent", + "children": [] + }, + { + "text": "statement context \"platform\" is invalid with object group", + "children": [] + }, + { + "text": "statement context \"platform\" is invalid with statementref", + "children": [] + }, + { + "text": "statement context \"platform\" is invalid with substatement", + "children": [] + }, + { + "text": "statement context \"platform\" is valid with empty objectType", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is invalid with object agent", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is invalid with object group", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is invalid with statementref", + "children": [] + }, + { + "text": "statement substatement context \"platform\" is valid with empty ObjectType", + "children": [] + } + ] + }, + { + "text": "A \"language\" property is a String", + "children": [ + { + "text": "statement context \"language\" is numeric", + "children": [] + }, + { + "text": "statement context \"language\" is object", + "children": [] + }, + { + "text": "statement substatement context \"language\" is numeric", + "children": [] + }, + { + "text": "statement substatement context \"language\" is object", + "children": [] + }, + { + "text": "statement context \"language\" is is invalid language", + "children": [] + }, + { + "text": "statement substatement context \"language\" is is invalid language", + "children": [] + } + ] + }, + { + "text": "A \"statement\" property is a Statement Reference", + "children": [ + { + "text": "statement context \"statement\" invalid with \"statementref\"", + "children": [] + }, + { + "text": "statement context \"statement\" invalid with \"id\" not UUID", + "children": [] + }, + { + "text": "statement substatement context \"statement\" invalid with \"statementref\"", + "children": [] + }, + { + "text": "statement substatement context \"statement\" invalid with \"id\" not UUID", + "children": [] + } + ] + }, + { + "text": "A \"contextActivities\" property's \"key\" has a value of \"parent\", \"grouping\", \"category\", or \"other\"", + "children": [ + { + "text": "statement context \"contextActivities\" is \"parent\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" is \"grouping\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" is \"category\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" is \"other\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" accepts all property keys \"parent\", \"grouping\", \"category\", and \"other\"", + "children": [] + }, + { + "text": "statement context \"contextActivities\" rejects any property key other than \"parent\", \"grouping\", \"category\", or \"other\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is \"parent\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is \"grouping\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is \"category\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" is \"other\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" accepts all property keys \"parent\", \"grouping\", \"category\", and \"other\"", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities\" rejects any property key other than \"parent\", \"grouping\", \"category\", or \"other\"", + "children": [] + } + ] + }, + { + "text": "A \"contextActivities\" property's \"value\" is an Activity", + "children": [ + { + "text": "statement context \"contextActivities parent\" value is activity array", + "children": [] + }, + { + "text": "statement context \"contextActivities grouping\" value is activity array", + "children": [] + }, + { + "text": "statement context \"contextActivities category\" value is activity array", + "children": [] + }, + { + "text": "statement context \"contextActivities other\" value is activity array", + "children": [] + }, + { + "text": "statement context contextActivities property's value is activity array with activities", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities parent\" value is activity array", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities grouping\" value is activity array", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities category\" value is activity array", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities other\" value is activity array", + "children": [] + }, + { + "text": "statement substatement context \"contextActivities property's value is activity array", + "children": [] + } + ] + }, + { + "text": "A \"contextAgents\" property is an array of \"contextAgent\" Objects.", + "children": [ + { + "text": "Statement with \"contextAgents\" property has an array of valid \"contextAgent\" Objects", + "children": [] + }, + { + "text": "Statement substatement with \"contextAgents\" property has an array of valid \"contextAgent\" Objects", + "children": [] + } + ] + }, + { + "text": "A \"contextAgents\" Object must have an \"objectType\" property of string \"contextAgent\" and a valid Agent Object \"agent\"", + "children": [ + { + "text": "Statement with \"contextAgents\" Object rejects statement if \"objectType\" property is anything other than string \"contextAgent\"", + "children": [] + }, + { + "text": "Statement with \"contextAgents\" Object rejects statement if \"agent\" property is invalid", + "children": [] + }, + { + "text": "Statement with \"contextAgents\" Object rejects statement if \"relevantTypes\" is empty", + "children": [] + }, + { + "text": "Statement with \"contextAgents\" Object rejects statement if \"relevantTypes\" contains non-IRI elements", + "children": [] + }, + { + "text": "Statement substatement with \"contextAgents\" Object rejects statement if \"objectType\" property is anything other than string \"contextAgent\"", + "children": [] + }, + { + "text": "Statement substatement with \"contextAgents\" Object rejects statement if \"agent\" property is invalid", + "children": [] + }, + { + "text": "Statement substatement with \"contextAgents\" Object rejects statement if \"relevantTypes\" is empty", + "children": [] + }, + { + "text": "Statement substatement with \"contextAgents\" Object rejects statement if \"relevantTypes\" contains non-IRI elements", + "children": [] + } + ] + }, + { + "text": "A \"contextGroups\" property is an array of \"contextGroup\" Objects", + "children": [ + { + "text": "Statement with \"contextGroups\" property has an array of valid \"contextGroup\" Objects", + "children": [] + }, + { + "text": "Statement substatement with \"contextGroups\" property has an array of valid \"contextGroup\" Objects", + "children": [] + } + ] + }, + { + "text": "A \"contextGroups\" Object must have an \"objectType\" property of string \"contextGroup\" and a valid Group Object \"group\"", + "children": [ + { + "text": "Statement with \"contextGroups\" Object rejects statement if \"objectType\" property is anything other than string \"contextGroup\"", + "children": [] + }, + { + "text": "Statement with \"contextGroups\" Object rejects statement if \"group\" property is invalid", + "children": [] + }, + { + "text": "Statement with \"contextGroups\" Object rejects statement if \"relevantTypes\" is empty", + "children": [] + }, + { + "text": "Statement with \"contextGroups\" Object rejects statement if \"relevantTypes\" contains non-IRI elements", + "children": [] + }, + { + "text": "Statement substatement with \"contextGroups\" Object rejects statement if \"objectType\" property is anything other than string \"contextGroup\"", + "children": [] + }, + { + "text": "Statement substatement with \"contextGroups\" Object rejects statement if \"group\" property is invalid", + "children": [] + }, + { + "text": "Statement substatement with \"contextGroups\" Object rejects statement if \"relevantTypes\" is empty", + "children": [] + }, + { + "text": "Statement substatement with \"contextGroups\" Object rejects statement if \"relevantTypes\" contains non-IRI elements", + "children": [] + } + ] + }, + { + "text": "An LRS returns a ContextActivity in an array, even if only a single ContextActivity is returned", + "children": [ + { + "text": "should return array for statement context \"parent\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement context \"grouping\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement context \"category\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement context \"other\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement substatement context \"parent\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement substatement context \"grouping\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement substatement context \"category\" when single ContextActivity is passed", + "children": [] + }, + { + "text": "should return array for statement substatement context \"other\" when single ContextActivity is passed", + "children": [] + } + ] + } + ] + }, + { + "text": "Timestamp Property Requirements", + "children": [ + { + "text": "A \"timestamp\" property is a TimeStamp", + "children": [ + { + "text": "statement \"template\" invalid string", + "children": [] + }, + { + "text": "statement \"template\" invalid date", + "children": [] + }, + { + "text": "statement \"template\" future date", + "children": [] + }, + { + "text": "substatement \"template\" invalid string", + "children": [] + }, + { + "text": "substatement \"template\" invalid date", + "children": [] + }, + { + "text": "substatement \"template\" future date", + "children": [] + } + ] + } + ] + }, + { + "text": "Stored Property Requirements", + "children": [ + { + "text": "An LRS MUST accept statements with the stored property", + "children": [ + { + "text": "using POST", + "children": [] + }, + { + "text": "using PUT", + "children": [] + } + ] + }, + { + "text": "A stored property must be a TimeStamp", + "children": [ + { + "text": "retrieve statements, test a stored property", + "children": [] + } + ] + } + ] + }, + { + "text": "Authority Property Requirements", + "children": [ + { + "text": "An LRS rejects with error code 400 Bad Request, a Request whose \"authority\" is a Group and consists of non-O-Auth Agents", + "children": [] + }, + { + "text": "An \"authority\" property is an Agent or Group", + "children": [ + { + "text": "should pass statement authority agent template", + "children": [] + }, + { + "text": "should pass statement authority template", + "children": [] + }, + { + "text": "should fail statement authority identified group (mbox)", + "children": [] + }, + { + "text": "should fail statement authority identified group", + "children": [] + }, + { + "text": "should fail statement authority identified group (openid)", + "children": [] + }, + { + "text": "should fail statement authority identified group (account)", + "children": [] + } + ] + }, + { + "text": "An \"authority\" property which is also a Group contains exactly two Agents", + "children": [ + { + "text": "statement \"authority\" invalid one member", + "children": [] + }, + { + "text": "statement \"authority\" invalid three member", + "children": [] + } + ] + }, + { + "text": "Statement authority shall only be an anonymous group with two members", + "children": [ + { + "text": "statement authority identified group is rejected", + "children": [] + }, + { + "text": "statement authority anonymous group with two members is accepted", + "children": [] + }, + { + "text": "statement authority anonymous group without two members is rejected", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request whose \"authority\" is a Group of more than two Agents", + "children": [ + { + "text": "statement \"authority\" invalid three member", + "children": [] + } + ] + }, + { + "text": "An LRS populates the \"authority\" property if it is not provided in the Statement, based on header information with the Agent corresponding to the user (contained within the header)", + "children": [ + { + "text": "should populate authority ", + "children": [] + } + ] + } + ] + }, + { + "text": "Version Property Requirements", + "children": [ + { + "text": "Statements returned by an LRS MUST retain the version property they are accepted with", + "children": [] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request which uses \"version\" and has the value set to anything but \"1.0\" or \"1.0.x\", where x is the semantic versioning number", + "children": [ + { + "text": "statement \"version\" valid 1.0", + "children": [] + }, + { + "text": "statement \"version\" valid 1.0.9", + "children": [] + }, + { + "text": "statement \"version\" invalid string", + "children": [] + }, + { + "text": "statement \"version\" invalid 0.9.9", + "children": [] + }, + { + "text": "statement \"version\" invalid 1.1.0", + "children": [] + } + ] + } + ] + }, + { + "text": "Attachments Property Requirements", + "children": [ + { + "text": "A Statement's \"attachments\" property is an array of Attachments", + "children": [ + { + "text": "statement \"attachments\" is an array", + "children": [] + }, + { + "text": "statement \"attachments\" not an array", + "children": [] + } + ] + }, + { + "text": "An Attachment is an Object", + "children": [ + { + "text": "statement \"attachment\" invalid numeric", + "children": [] + }, + { + "text": "statement \"attachment\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"usageType\" property is an IRI", + "children": [ + { + "text": "statement \"usageType\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"contentType\" property is an Internet Media/MIME type", + "children": [ + { + "text": "statement \"contentType\" invalid number", + "children": [] + } + ] + }, + { + "text": "A \"length\" property is an Integer", + "children": [ + { + "text": "statement \"length\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"sha2\" property is a String", + "children": [ + { + "text": "statement \"sha2\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"fileUrl\" property is an IRL", + "children": [ + { + "text": "statement \"fileUrl\" invalid string", + "children": [] + } + ] + }, + { + "text": "A \"display\" property is a Language Map", + "children": [ + { + "text": "statement attachment \"display\" language map numeric", + "children": [] + }, + { + "text": "statement attachment \"display\" language map string", + "children": [] + }, + { + "text": "statement attachment \"description\" language map numeric", + "children": [] + }, + { + "text": "statement attachment \"description\" language map string", + "children": [] + } + ] + } + ] + }, + { + "text": "Retrieval of Statements", + "children": [ + { + "text": "A \"statements\" property which is too large for a single page will create a container for each additional page", + "children": [] + }, + { + "text": "A \"more\" property's referenced container object follows the same rules as the original GET request, originating with a single \"statements\" property and a single \"more\" property", + "children": [] + }, + { + "text": "An LRS's Statement API, upon processing a successful GET request, will return a single \"statements\" property and a single \"more\" property.", + "children": [ + { + "text": "will return single statements property and may return", + "children": [] + } + ] + }, + { + "text": "A \"statements\" property is an Array of Statements", + "children": [ + { + "text": "should return StatementResult with statements as array using GET without \"statementId\" or \"voidedStatementId\"", + "children": [] + } + ] + }, + { + "text": "The \"more\" property is absent or an empty string (no whitespace) if the entire results of the original GET request have been returned.", + "children": [ + { + "text": "should return empty \"more\" property or no \"more\" property when all statements returned", + "children": [] + } + ] + }, + { + "text": "If not empty, the \"more\" property's IRL refers to a specific container object corresponding to the next page of results from the orignal GET request", + "children": [ + { + "text": "should return \"more\" which refers to next page of results", + "children": [] + } + ] + } + ] + }, + { + "text": "Signed Statements", + "children": [ + { + "text": "LRS must validate and store statement signatures if they are provided", + "children": [ + { + "text": "A Signed Statement MUST include a JSON web signature, JWS", + "children": [ + { + "text": "rejects a signed statement with a malformed signature - bad content type", + "children": [] + }, + { + "text": "rejects a signed statement with a malformed signature - bad JWS", + "children": [] + } + ] + }, + { + "text": "The JWS signature MUST have a payload of a valid JSON serialization of the complete Statement before the signature was added.", + "children": [ + { + "text": "rejects statement with invalid JSON serialization", + "children": [] + } + ] + }, + { + "text": "The JWS signature MUST use an algorithm of \"RS256\", \"RS384\", or \"RS512\".", + "children": [ + { + "text": "Accepts signed statement with \"RS256\"", + "children": [] + }, + { + "text": "Accepts signed statement with \"RS384\"", + "children": [] + }, + { + "text": "Accepts signed statement with \"RS512\"", + "children": [] + }, + { + "text": "Rejects signed statement with another algorithm", + "children": [] + } + ] + } + ] + } + ] + }, + { + "text": "Special Data Types and Rules", + "children": [ + { + "text": "An Extension is defined as an Object of any \"extensions\" property", + "children": [ + { + "text": "statement activity extensions valid boolean", + "children": [] + }, + { + "text": "statement activity extensions valid numeric", + "children": [] + }, + { + "text": "statement activity extensions valid object", + "children": [] + }, + { + "text": "statement activity extensions valid string", + "children": [] + }, + { + "text": "statement result extensions valid boolean", + "children": [] + }, + { + "text": "statement result extensions valid numeric", + "children": [] + }, + { + "text": "statement result extensions valid object", + "children": [] + }, + { + "text": "statement result extensions valid string", + "children": [] + }, + { + "text": "statement context extensions valid boolean", + "children": [] + }, + { + "text": "statement context extensions valid numeric", + "children": [] + }, + { + "text": "statement context extensions valid object", + "children": [] + }, + { + "text": "statement context extensions valid string", + "children": [] + }, + { + "text": "statement substatement activity extensions valid boolean", + "children": [] + }, + { + "text": "statement substatement activity extensions valid numeric", + "children": [] + }, + { + "text": "statement substatement activity extensions valid object", + "children": [] + }, + { + "text": "statement substatement activity extensions valid string", + "children": [] + }, + { + "text": "statement substatement result extensions valid boolean", + "children": [] + }, + { + "text": "statement substatement result extensions valid numeric", + "children": [] + }, + { + "text": "statement substatement result extensions valid object", + "children": [] + }, + { + "text": "statement substatement result extensions valid string", + "children": [] + }, + { + "text": "statement substatement context extensions valid boolean", + "children": [] + }, + { + "text": "statement substatement context extensions valid numeric", + "children": [] + }, + { + "text": "statement substatement context extensions valid object", + "children": [] + }, + { + "text": "statement substatement context extensions valid string", + "children": [] + } + ] + }, + { + "text": "An Extension can be null, an empty string, objects with nothing in them when using POST.", + "children": [ + { + "text": "statement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement activity extension values can be empty string", + "children": [] + }, + { + "text": "statement activity extension values can be null", + "children": [] + }, + { + "text": "statement activity extension values can be empty object", + "children": [] + }, + { + "text": "statement result extensions can be empty object", + "children": [] + }, + { + "text": "statement result extension values can be empty string", + "children": [] + }, + { + "text": "statement result extension values can be null", + "children": [] + }, + { + "text": "statement result extension values can be empty object", + "children": [] + }, + { + "text": "statement context extensions can be empty object", + "children": [] + }, + { + "text": "statement context extensions can be empty string", + "children": [] + }, + { + "text": "statement context extensions can be null", + "children": [] + }, + { + "text": "statement context extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement activity extension values can be empty string", + "children": [] + }, + { + "text": "statement substatement activity extension values can be null", + "children": [] + }, + { + "text": "statement substatement activity extension values can be empty object", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty string", + "children": [] + }, + { + "text": "statement substatement result extensions can be null", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty string", + "children": [] + }, + { + "text": "statement substatement context extensions can be null", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty object", + "children": [] + } + ] + }, + { + "text": "An Extension \"key\" is an IRI", + "children": [ + { + "text": "statement activity extensions key is not an IRI", + "children": [] + }, + { + "text": "statement result extensions key is not an IRI", + "children": [] + }, + { + "text": "statement context extensions key is not an IRI", + "children": [] + }, + { + "text": "statement substatement activity extensions key is not an IRI", + "children": [] + }, + { + "text": "statement substatement result extensions key is not an IRI", + "children": [] + }, + { + "text": "statement substatement context extensions key is not an IRI", + "children": [] + } + ] + }, + { + "text": "An Extension can be null, an empty string, objects with nothing in them when using PUT.", + "children": [ + { + "text": "statement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement activity extension values can be empty string", + "children": [] + }, + { + "text": "statement activity extension values can be null", + "children": [] + }, + { + "text": "statement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement result extensions can be empty object", + "children": [] + }, + { + "text": "statement result extension values can be empty string", + "children": [] + }, + { + "text": "statement result extension values can be null", + "children": [] + }, + { + "text": "statement result extension values can be empty object", + "children": [] + }, + { + "text": "statement context extensions can be empty object", + "children": [] + }, + { + "text": "statement context extension values can be empty string", + "children": [] + }, + { + "text": "statement context extension values can be null", + "children": [] + }, + { + "text": "statement context extension values can be empty object", + "children": [] + }, + { + "text": "statement substatement activity extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement activity extension values can be empty string", + "children": [] + }, + { + "text": "statement substatement activity extension values can be null", + "children": [] + }, + { + "text": "statement substatement activity extension values can be empty object", + "children": [] + }, + { + "text": "statement substatement result extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement result extension values can be empty string", + "children": [] + }, + { + "text": "statement substatement result extension values can be null", + "children": [] + }, + { + "text": "statement substatement result extension values can be empty object", + "children": [] + }, + { + "text": "statement substatement context extensions can be empty object", + "children": [] + }, + { + "text": "statement substatement context extension values can be empty string", + "children": [] + }, + { + "text": "statement substatement context extension values can be null", + "children": [] + }, + { + "text": "statement substatement context extension values can be empty object", + "children": [] + } + ] + }, + { + "text": "A Language Map follows RFC5646", + "children": [ + { + "text": "statement verb \"display\" language map invalid", + "children": [] + }, + { + "text": "statement object \"name\" language map invalid", + "children": [] + }, + { + "text": "statement object \"description\" language map invalid", + "children": [] + }, + { + "text": "statement attachment \"display\" language map invalid", + "children": [] + }, + { + "text": "statement attachment \"description\" language map invalid", + "children": [] + }, + { + "text": "statement substatement verb \"display\" language map invalid", + "children": [] + }, + { + "text": "statement substatement activity \"name\" language map invalid", + "children": [] + }, + { + "text": "statement substatement activity \"description\" language map invalid", + "children": [] + } + ] + }, + { + "text": "A TimeStamp is defined as a Date/Time formatted according to ISO 8601", + "children": [ + { + "text": "statement \"template\" invalid string in timestamp", + "children": [] + }, + { + "text": "statement \"template\" invalid date in timestamp", + "children": [] + }, + { + "text": "statement \"template\" invalid date in timestamp: did not reject statement timestamp with -00 offset", + "children": [] + }, + { + "text": "statement \"template\" invalid date in timestamp: did not reject statement timestamp with -0000 offset", + "children": [] + }, + { + "text": "statement \"template\" invalid date in timestamp: did not reject statement timestamp with -00:00 offset", + "children": [] + }, + { + "text": "Statement \"template\" valid RFC 3339 date in timestamp", + "children": [] + }, + { + "text": "substatement \"template\" invalid string in timestamp", + "children": [] + }, + { + "text": "substatement \"template\" invalid date in timestamp", + "children": [] + }, + { + "text": "substatement \"template\" invalid date in timestamp: did not reject substatement timestamp with -00 offset", + "children": [] + }, + { + "text": "substatement \"template\" invalid date in timestamp: did not reject substatement timestamp with -0000 offset", + "children": [] + }, + { + "text": "substatement \"template\" invalid date in timestamp: did not reject substatement timestamp with -00:00 offset", + "children": [] + }, + { + "text": "Substatement \"template\" valid RFC 3339 date in timestamp", + "children": [] + } + ] + }, + { + "text": "A Timestamp MUST preserve precision to at least milliseconds, 3 decimal points beyond seconds.", + "children": [ + { + "text": "retrieve statements, test a timestamp property", + "children": [] + }, + { + "text": "retrieve statements, test a stored property", + "children": [] + } + ] + }, + { + "text": "A Duration MUST be expressed using the format for Duration in ISO 8601:2004(E) section 4.4.3.2.", + "children": [ + { + "text": "Statement result \"duration\" property is valid", + "children": [] + }, + { + "text": "Statement substatement result \"duration\" property is valid", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with invalid string", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with invalid number", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid invalid number", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with invalid object", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid with invalid object", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with invalid duration", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is invalid with invalid duration", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid with \"PT4H35M59.14S\"", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid with \"PT16559.14S\"", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid with \"P3Y1M29DT4H35M59.14S\"", + "children": [] + }, + { + "text": "statement substatement result \"duration\" property is valid with \"P3Y\"", + "children": [] + }, + { + "text": "statement result \"duration\" property is valid with \"P4W\"", + "children": [] + }, + { + "text": "statement result \"duration\" property is invalid with \"P4W1D\"", + "children": [] + } + ] + } + ] + }, + { + "text": "HEAD Request Implementation Requirements", + "children": [ + { + "text": "An LRS accepts HEAD requests without Content-Length headers", + "children": [] + }, + { + "text": "An LRS accepts GET requests without Content-Length headers", + "children": [] + }, + { + "text": "An LRS accepts HEAD requests", + "children": [ + { + "text": "should succeed HEAD activities with no body", + "children": [] + }, + { + "text": "should succeed HEAD activities profile with no body", + "children": [] + }, + { + "text": "should succeed HEAD activities state with no body", + "children": [] + }, + { + "text": "should succeed HEAD agents with no body", + "children": [] + }, + { + "text": "should succeed HEAD agents profile with no body", + "children": [] + }, + { + "text": "should succeed HEAD statements with no body", + "children": [] + } + ] + }, + { + "text": "An LRS responds to a HEAD request in the same way as a GET request, but without the message-body", + "children": [ + { + "text": "should succeed HEAD activities with no body", + "children": [] + }, + { + "text": "should succeed HEAD activities profile with no body", + "children": [] + }, + { + "text": "should succeed HEAD activities state with no body", + "children": [] + }, + { + "text": "should succeed HEAD agents with no body", + "children": [] + }, + { + "text": "should succeed HEAD agents profile with no body", + "children": [] + }, + { + "text": "should succeed HEAD statements with no body", + "children": [] + } + ] + } + ] + }, + { + "text": "Alternate Request Syntax Requirements", + "children": [ + { + "text": "The LRS does NOT allow Alternate Request Syntax in xAPI 2.0", + "children": [ + { + "text": "An LRS rejects POST requests containing method query parameters", + "children": [] + }, + { + "text": "An LRS rejects an alternate request syntax not issued as a POST", + "children": [] + }, + { + "text": "An LRS REJECTS an alternate request syntax PUT issued as a POST", + "children": [] + }, + { + "text": "During an alternate request syntax the LRS treats the listed form parameters, 'Authorization', 'X-Experience-API-Version', 'Content-Type', 'Content-Length', 'If-Match' and 'If-None-Match', as header parameters", + "children": [] + }, + { + "text": "An LRS will reject an alternate request syntax which contains any extra information with error code 400 Bad Request", + "children": [] + }, + { + "text": "An LRS will reject an alternate request syntax sending content which does not have a form parameter with the name of \"content\"", + "children": [ + { + "text": "will pass PUT with content body which is url encoded", + "children": [] + }, + { + "text": "will fail PUT with no content body", + "children": [] + }, + { + "text": "will fail PUT with content body which is not url encoded", + "children": [] + } + ] + } + ] + } + ] + }, + { + "text": "Encoding Requirements", + "children": [ + { + "text": "All Strings are encoded and interpreted as UTF-8", + "children": [] + } + ] + }, + { + "text": "Document Resource Requirements", + "children": [ + { + "text": "An LRS makes no modifications to stored data for any rejected request", + "children": [] + }, + { + "text": "A Document Merge overwrites any duplicate Objects from the previous document with the new document.", + "children": [] + }, + { + "text": "A Document Merge only performs overwrites at one level deep, although the entire object is replaced.", + "children": [] + } + ] + }, + { + "text": "Agents Resource Requirements", + "children": [ + { + "text": "An LRS has an Agents Resource with endpoint \"base IRI\" + /agents\"", + "children": [] + }, + { + "text": "An LRS's Agents Resource accepts GET requests", + "children": [] + }, + { + "text": "An LRS's Agent Resource upon processing a successful GET request returns a Person Object if the \"agent\" parameter can be found in the LRS and code 200 OK", + "children": [] + }, + { + "text": "An LRS's Agents Resource rejects a GET request without \"agent\" as a parameter with error code 400 Bad Request", + "children": [] + }, + { + "text": "A Person Object's \"objectType\" property is a String and is \"Person\"", + "children": [] + }, + { + "text": "A Person Object's \"name\" property is an Array of Strings", + "children": [] + }, + { + "text": "A Person Object's \"mbox\" property is an Array of IRIs", + "children": [] + }, + { + "text": "A Person Object's \"mbox\" entries have the form \"mailto:emailaddress\"", + "children": [] + }, + { + "text": "A Person Object's \"mbox_sha1sum\" property is an Array of Strings", + "children": [] + }, + { + "text": "A Person Object's \"openid\" property is an Array of Strings", + "children": [] + }, + { + "text": "A Person Object's \"account\" property is an Array of Account Objects", + "children": [] + }, + { + "text": "An LRSs Agents Resource rejects a GET request with \"agent\" as a parameter if it is not a valid, in structure, Agent with error code 400 Bad Request", + "children": [] + } + ] + }, + { + "text": "About Resource Requirements", + "children": [ + { + "text": "An LRS has an About Resource with endpoint \"base IRI\"+\"/about\"", + "children": [] + }, + { + "text": "An LRS's About Resource upon processing a successful GET request returns a version property and code 200 OK", + "children": [] + }, + { + "text": "An LRS's About Resource's version property is an array of strings", + "children": [] + }, + { + "text": "An LRS's About Resource's version property contains at least one string of \"2.0.0\"", + "children": [] + }, + { + "text": "An LRS's About Resource's version property can only have values of \"0.9\", \"0.95\", \"1.0.0\", or \"\"1.0.\" + X\" with", + "children": [] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request which does not use a \"X-Experience-API-Version\" header name to any Resource except the About Resource", + "children": [ + { + "text": "using Statement Endpoint", + "children": [] + }, + { + "text": "using Activities Endpoint", + "children": [] + }, + { + "text": "using Activities Profile Endpoint", + "children": [] + }, + { + "text": "using Activities State Endpoint", + "children": [] + }, + { + "text": "using Agents Endpoint", + "children": [] + }, + { + "text": "using Agents Profile Endpoint", + "children": [] + } + ] + } + ] + }, + { + "text": "Error Codes Requirements", + "children": [ + { + "text": "An LRS rejects with error code 400 Bad Request any request to an Resource which uses a parameter not recognized by the LRS", + "children": [] + }, + { + "text": "An LRS rejects with error code 400 Bad Request any request to an Resource which uses a parameter with differing case", + "children": [ + { + "text": "should fail on PUT statement when not using \"statementId\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"statementId\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"voidedStatementId\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"agent\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"verb\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"activity\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"registration\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"related_activities\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"related_agents\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"since\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"until\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"limit\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"format\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"attachments\"", + "children": [] + }, + { + "text": "should fail on GET statement when not using \"ascending\"", + "children": [] + } + ] + }, + { + "text": "An LRS does not process any batch of Statements in which one or more Statements is rejected and if necessary, restores the LRS to the state in which it was before the batch began processing", + "children": [ + { + "text": "should not persist any statements on a single failure", + "children": [] + } + ] + } + ] + }, + { + "text": "Versioning Requirements", + "children": [ + { + "text": "An LRS sends a header response with \"X-Experience-API-Version\" as the name and the latest patch version after \"1.0.0\" as the value", + "children": [] + }, + { + "text": "An LRS will not modify Statements based on a \"version\" before \"1.0.1\"", + "children": [ + { + "text": "should not convert newer version format to prior version format", + "children": [] + } + ] + }, + { + "text": "An LRS rejects with error code 400 Bad Request, a Request which does not use a \"X-Experience-API-Version\" header name to any Resource except the About Resource", + "children": [ + { + "text": "Should pass when About GET without header \"X-Experience-API-Version\"", + "children": [] + }, + { + "text": "Should fail when Statement GET without header \"X-Experience-API-Version\"", + "children": [] + }, + { + "text": "Should fail when Statement POST without header \"X-Experience-API-Version\"", + "children": [] + }, + { + "text": "Should fail when Statement PUT without header \"X-Experience-API-Version\"", + "children": [] + } + ] + } + ] + }, + { + "text": "Authentication Requirements", + "children": [ + { + "text": "An LRS must support HTTP Basic Authentication", + "children": [] + }, + { + "text": "An LRS rejects a Statement of bad authorization, either authentication needed or failed credentials, with error code 401 Unauthorized", + "children": [ + { + "text": "fails when given a random name pass pair", + "children": [] + }, + { + "text": "fails with a malformed header", + "children": [] + } + ] + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/bin/OAuth.js b/bin/OAuth.js index 1615dd94..d01b0f05 100644 --- a/bin/OAuth.js +++ b/bin/OAuth.js @@ -1,9 +1,4 @@ -var request = require("request"); -var OAuth1 = require('oauth-1.0a'); var express = require("express"); - -var questions = require('questions'); - var OAuth = require('oauth').OAuth; diff --git a/bin/console_runner.js b/bin/console_runner.js index c44254a0..4b09473a 100644 --- a/bin/console_runner.js +++ b/bin/console_runner.js @@ -3,8 +3,9 @@ var TestRunner = require('./testRunner.js').testRunner; var jsonSchema = require('jsonschema'); var validate = jsonSchema.validate; var colors = require('colors'); -var libpath = require('path'), - fs = require('fs'); +var libpath = require('path'); +var fs = require('fs'); +const specConfig = require("../specConfig"); require('pretty-error').start(); @@ -17,39 +18,41 @@ function clean_dir(val, dir) { } program - .version('0.0.1') - .option('-e, --endpoint [url]', 'xAPI endpoint') + .version('0.0.2') + .option('-x, --xapiVersion [string]', '🌟 New: Version of the xAPI spec to test against') + .option('-e, --endpoint [url]', 'xAPI Endpoint') .option('-u, --authUser [string]', 'Basic Auth Username') .option('-p, --authPassword [string]', 'Basic Auth Password') .option('-a, --basicAuth', 'Enable Basic Auth') .option('-o, --oAuth1', 'Enable oAuth 1') .option('-c, --consumer_key [string]', 'oAuth 1 Consumer Key') .option('-s, --consumer_secret [string]', 'oAuth 1 Consumer Secret') - .option('-r, --request_token_path [string]', 'Path to OAuth request token endpoint (relative to endpoint)') - .option('-t, --auth_token_path [string]', 'Path to OAuth authorization token endpoint (relative to endpoint)') - .option('-l, --authorization_path [string]', 'Path to OAuth user authorization endpoint (relative to endpoint)') - .option('-g, --grep [string]', 'Only run tests that match the given pattern') - .option('-b, --bail', 'Abort the battery if one test fails') - .option('-d, --directory [value]', 'Specific directories of tests (as a comma seperated list with no spaces)', clean_dir, ['v1_0_3']) - .option('-z, --errors', 'Results log of failing tests only') + .option('-r, --request_token_path [string]', 'Path to OAuth request token endpoint (relative to endpoint).') + .option('-t, --auth_token_path [string]', 'Path to OAuth authorization token endpoint (relative to endpoint).') + .option('-l, --authorization_path [string]', 'Path to OAuth user authorization endpoint (relative to endpoint).') + .option('-g, --grep [string]', 'Only run tests that match the given pattern.') + .option('-b, --bail', 'Abort the battery if one test fails.') + .option('-d, --directory [value]', 'Specific directories of tests (as a comma-separated list with no spaces).', clean_dir, [...[]]) + .option('-z, --errors', 'Results log of failing tests only.') .parse(process.argv); var options = { - endpoint: program.endpoint, - authUser: program.authUser, - authPass: program.authPassword, - basicAuth: program.basicAuth, - oAuth1: program.oAuth1, - consumer_key: program.consumer_key, - consumer_secret: program.consumer_secret, - request_token_path: program.request_token_path, - auth_token_path: program.auth_token_path, - authorization_path: program.authorization_path, - grep: program.grep, - bail: program.bail, - directory: program.directory, - errors: program.errors - } + xapiVersion: program.xapiVersion, + endpoint: program.endpoint, + authUser: program.authUser, + authPass: program.authPassword, + basicAuth: program.basicAuth, + oAuth1: program.oAuth1, + consumer_key: program.consumer_key, + consumer_secret: program.consumer_secret, + request_token_path: program.request_token_path, + auth_token_path: program.auth_token_path, + authorization_path: program.authorization_path, + grep: program.grep, + bail: program.bail, + directory: program.directory, + errors: program.errors +} /* var valid = validate(options, { @@ -90,23 +93,23 @@ if (valid.errors.length) { var testRunner = null; -//catches ctrl+c event +// Catches Ctrl+C event. process.on('SIGINT', function() { - console.log(colors.white('Aborting tests')); + console.log(colors.white('Aborting tests.')); testRunner.cancel(); }); process.on('exit', function() { console.log(colors.white('Closed')); -}) +}); function start(options) { //These are already used to fetch the access token, and are not needed by the runer - delete options.request_token_path ; - delete options.auth_token_path ; - delete options.authorization_path ; + delete options.request_token_path; + delete options.auth_token_path; + delete options.authorization_path; testRunner = new TestRunner('console', null, options); testRunner.start(); @@ -147,7 +150,7 @@ function start(options) return temp; } - // write log to file + // Write log to file. var cleanLog = testRunner.getCleanRecord(); var output; if (options.errors) { diff --git a/bin/lrs-test.js b/bin/lrs-test.js index 16084c96..ab458e85 100755 --- a/bin/lrs-test.js +++ b/bin/lrs-test.js @@ -1,4 +1,7 @@ #!/usr/bin/env node + +const specConfig = require('../specConfig'); + /** * Description : This is the command line interface for running the lrs conformance test suite. * @@ -40,7 +43,8 @@ function runTests(_options) { var optionsValidator = Joi.object({ - directory: Joi.array().items(Joi.string().required()), + xapiVersion: Joi.string(), + directory: Joi.array().items(Joi.string()), /* See [RFC-3986](http://tools.ietf.org/html/rfc3986#page-17) */ endpoint: Joi.string().regex(/^[a-zA-Z][a-zA-Z0-9+\.-]*:.+/, 'URI').required(), grep: Joi.string(), @@ -86,9 +90,66 @@ process.exit(); } - var DIRECTORY = ['v1_0_3']; + let endpointSpecified = _options.endpoint != undefined; + let versionSpecified = _options.xapiVersion != undefined; + + let directorySpecified = Array.isArray(_options.directory) && _options.directory.length > 0; + let defaultDirectory = specConfig.specToFolder[specConfig.defaultVersion]; + + if (!endpointSpecified) { + console.error(`You must specify an endpoint (-e or --endpoint) for your LRS.`); + console.error(`LRS endpoints typically have the form: https://lrs.net/xapi.`); + process.exit(1); + } + + if (versionSpecified && directorySpecified) { + console.error(`Cannot specify both an xAPI Version and a Directory.`); + process.exit(1); + } + + // Set up a directory based on whether or not we provided an xAPI version + if (versionSpecified) { + let versionFolder = specConfig.specToFolder[_options.xapiVersion]; + if (versionFolder != undefined) + _options.directory = [versionFolder]; + + else { + console.error(`Unknown version of the xAPI spec: ${_options.xapiVersion}. Unable to find appropriate test suite.`); + process.exit(1); + } + } + + else if (directorySpecified) { + let matchingSpec = undefined; + for (let dir of _options.directory) { + let spec = specConfig.getSpecFromFolder(dir); + if (spec != matchingSpec) { + if (matchingSpec == undefined) + matchingSpec = spec; + else { + console.error(`Multiple directories specified which refer to different versions of the xAPI spec: ${spec} vs. ${matchingSpec}`); + process.exit(1); + } + } + } + + if (matchingSpec == undefined) { + console.error(`Unable to determine which version of xAPI to test against with diectories: ${_options.directory.join(", ")}`); + process.exit(1); + } + + _options.xapiVersion = matchingSpec; + } + + if (!versionSpecified && !directorySpecified) { + _options.xapiVersion = specConfig.defaultVersion; + _options.directory = [defaultDirectory]; + console.warn(`No xAPI version or manual path specified -- defaulting to ${specConfig.defaultVersion}.`); + } + var options = { - directory: _options.directory || DIRECTORY, + xapiVersion: _options.xapiVersion, + directory: _options.directory, endpoint: _options.endpoint, basicAuth: _options.basicAuth, authUser: _options.authUser, @@ -119,17 +180,24 @@ bail: options.bail }); + console.log(` + \r\bAttempting xAPI Conformance Suite Against: + \r\r xAPI Version: ${options.xapiVersion} + \r\r Test Path(s): ${options.directory} + \r\r LRS Endpoint: ${options.endpoint} + `); + console.log("Grep is " + grep); process.env.DIRECTORY = options.directory[0]; - //adds optional tests to the front in ascending order + // Adds optional tests to the front in ascending order. if (options.optional){ options.optional.reverse().forEach(function(dir) { options.directory.unshift(dir); }); } - console.log("directory is ", options.directory); + // console.log("directory is ", options.directory); process.env.LRS_ENDPOINT = options.endpoint; @@ -137,7 +205,7 @@ process.env.BASIC_AUTH_USER = options.authUser; process.env.BASIC_AUTH_PASSWORD = options.authPass; process.env.OAUTH1_ENABLED = options.oAuth1; - + process.env.XAPI_VERSION = options.xapiVersion; if(options.oAuth1) { diff --git a/bin/testRunner.js b/bin/testRunner.js index c970bfb5..0c3eaa6c 100644 --- a/bin/testRunner.js +++ b/bin/testRunner.js @@ -1,23 +1,20 @@ 'use strict'; -const child_process = require('child_process'), - libpath = require('path'), - fs = require('fs'), - EventEmitter = require('events').EventEmitter, - rollup = require('./rollupRules.js'), - version = require('../version.js'), - SpecRefs = require('../test/references.json'); +const child_process = require('child_process'); +const libpath = require('path'); +const EventEmitter = require('events').EventEmitter; +const rollup = require('./rollupRules.js'); +const version = require('../version.js'); +const SpecRefs = require('../test/references.json'); class Suite { - constructor(title) - { + constructor(title) { var match; this.log = ""; this.title = title; - if(match = /\(([^\)]*\d[^\)]*)\)/.exec(title)) - { + if (match = /\(([^\)]*\d[^\)]*)\)/.exec(title)) { this.name = title.slice(0, match.index).trim(); - if(SpecRefs[this.name]){ + if (SpecRefs[this.name]) { var data = SpecRefs[this.name]; this.requirement = data['1.0.3_link'] || data['1.0.3_ref'] || data['1.0.2_ref_text']; } @@ -33,20 +30,17 @@ class Suite { this.parent = null; this.tests = []; } - addTest(test){ + addTest(test) { this.tests.push(test); test.parent = this; } - _log(data) - { - this.log+=data; + _log(data) { + this.log += data; } } -class TestRunner extends EventEmitter -{ - constructor(name, owner, flags, lrsSettingsUUID,options, rollupRule) - { +class TestRunner extends EventEmitter { + constructor(name, owner, flags, lrsSettingsUUID, options, rollupRule) { super(); this.proc = null; @@ -58,6 +52,8 @@ class TestRunner extends EventEmitter this.lrsSettingsUUID = lrsSettingsUUID; this.rollupRule = rollup[rollupRule] ? rollupRule : 'mustPassAll'; + this.xapiVersion = (flags.xapiVersion || version.versionNumber); + this.uuid = require('uuid').v4(); this.startTime = null; this.endTime = null; @@ -75,144 +71,135 @@ class TestRunner extends EventEmitter this.activeTest = null; } - start() - { - if(this.state !== 'notStarted') return; + start() { + if (this.state !== 'notStarted') return; this.state = 'started'; - // spin up the child process - this.proc = child_process.fork( libpath.join(__dirname, "lrs-test.js"), + // Spin up the child process. + this.proc = child_process.fork(libpath.join(__dirname, "lrs-test.js"), ["--debug"], { - execArgv:[/*"--debug-brk=5959"*/], - cwd: libpath.join(__dirname,"/../") + execArgv: [/*"--debug-brk=5959"*/], + cwd: libpath.join(__dirname, "/../") } ); - // hook up listeners + // Hook up listeners. this._registerStatusUpdates(); - // kick off tests when ready - this.proc.on('message', function(msg) - { - if(msg.action === 'ready'){ + // Kick off tests when ready. + this.proc.on('message', function (msg) { + if (msg.action === 'ready') { //this is still a bit of a mess - we'll build the actual settings from this.flags and this.options var flags = JSON.parse(JSON.stringify(this.flags)); - if(this.options && this.options.grep) + if (this.options && this.options.grep) flags.grep = this.options.grep; - if(this.options && this.options.optional) + if (this.options && this.options.optional) flags.optional = this.options.optional; - this.proc.send({action: 'runTests', payload: flags}); + this.proc.send({ action: 'runTests', payload: flags }); } }.bind(this)); } - _registerStatusUpdates() - { + _registerStatusUpdates() { - this.proc.on('message', function(msg) - { + this.proc.on('message', function (msg) { var action = msg.action, payload = msg.payload; - switch(action) - { - case 'start': - - // initialize counters - this.summary.total = payload; - this.summary.passed = 0; - this.summary.failed = 0; - this.startTime = Date.now(); - this.summary.version = version.versionNumber; - break; + switch (action) { + case 'start': + + // initialize counters + this.summary.total = payload; + this.summary.passed = 0; + this.summary.failed = 0; + this.startTime = Date.now(); + this.summary.version = this.xapiVersion; + break; - case 'data': + case 'data': - if(this.activeTest) - this.activeTest._log(payload); - break; + if (this.activeTest) + this.activeTest._log(payload); + break; - case 'end': + case 'end': - this.endTime = Date.now(); - this.duration = this.endTime - this.startTime; - this.state = 'finished'; - break; + this.endTime = Date.now(); + this.duration = this.endTime - this.startTime; + this.state = 'finished'; + break; - case 'suite start': + case 'suite start': - // start a new suite - var newSuite = new Suite(payload); + // start a new suite + var newSuite = new Suite(payload); - // add to log - if(this.activeTest) - this.activeTest.addTest(newSuite); - else - this.log = newSuite; + // add to log + if (this.activeTest) + this.activeTest.addTest(newSuite); + else + this.log = newSuite; - this.activeTest = newSuite; - break; + this.activeTest = newSuite; + break; - case 'suite end': + case 'suite end': - if(this.activeTest) - { - // finish the suite - if(this.activeTest.title === payload) - { - // roll up test status - this.activeTest.status = rollup[this.rollupRule](this.activeTest); + if (this.activeTest) { + // finish the suite + if (this.activeTest.title === payload) { + // roll up test status + this.activeTest.status = rollup[this.rollupRule](this.activeTest); - // move test cursor - this.activeTest = this.activeTest.parent; + // move test cursor + this.activeTest = this.activeTest.parent; + } + else + console.error('Dangling suite end!', this.activeTest.title); } - else - console.error('Dangling suite end!', this.activeTest.title); - } - break; + break; - case 'test start': + case 'test start': - // start a new test - var newTest = new Suite(payload); + // start a new test + var newTest = new Suite(payload); - // add to log - if(this.activeTest) - this.activeTest.addTest(newTest); - else - this.log = newTest; + // add to log + if (this.activeTest) + this.activeTest.addTest(newTest); + else + this.log = newTest; - this.activeTest = newTest; - break; + this.activeTest = newTest; + break; - case 'test end': + case 'test end': - if(this.activeTest) - { - if(this.activeTest.title === payload) - this.activeTest = this.activeTest.parent; - else - console.error('Dangling test end!', this.activeTest.title); + if (this.activeTest) { + if (this.activeTest.title === payload) + this.activeTest = this.activeTest.parent; + else + console.error('Dangling test end!', this.activeTest.title); + break; + } + + case 'test pass': + if (this.activeTest) { + this.activeTest.status = 'passed'; + this.summary.passed++; + } + break; + + case 'test fail': + if (this.activeTest) { //careful - cancel can blank this, then the message comes in + this.activeTest.status = 'failed'; + this.activeTest.error = payload.message; + this.summary.failed++; + } break; - } - - case 'test pass': - if(this.activeTest) - { - this.activeTest.status = 'passed'; - this.summary.passed++; - } - break; - - case 'test fail': - if(this.activeTest){ //careful - cancel can blank this, then the message comes in - this.activeTest.status = 'failed'; - this.activeTest.error = payload.message; - this.summary.failed++; - } - break; }; // pass along the event @@ -220,52 +207,43 @@ class TestRunner extends EventEmitter }.bind(this)); - this.proc.on("close", function(w) - { - if(this.state == "cancelled" || this.state == "finished") - { + this.proc.on("close", function (w) { + if (this.state == "cancelled" || this.state == "finished") { return; } - else - { + else { this.state = "error"; - this.emit('message', {action: 'end'}); + this.emit('message', { action: 'end' }); this.emit('close'); } }.bind(this)); } - cancel() - { - if(this.proc) - { + cancel() { + if (this.proc) { this.endTime = Date.now(); this.duration = this.endTime - this.startTime; - // evaluate all in-progress suites to "cancelled" + // Evaluate all in-progress suites to "cancelled". this.state = 'cancelled'; this.proc.kill(); - if(this.activeTest) - { + if (this.activeTest) { this.activeTest.status = 'cancelled'; this.activeTest = this.activeTest.parent; } - while(this.activeTest){ + while (this.activeTest) { this.activeTest.status = rollup[this.rollupRule](this.activeTest); - this.emit('message', {action: 'suite end', payload: this.activeTest.title}); + this.emit('message', { action: 'suite end', payload: this.activeTest.title }); this.activeTest = this.activeTest.parent; } - this.emit('message', {action: 'end'}); + this.emit('message', { action: 'end' }); this.emit('close'); } } - getCleanRecord() - { - - + getCleanRecord() { var runRecord = { name: this.name || null, owner: this.owner || null, @@ -275,10 +253,10 @@ class TestRunner extends EventEmitter authUser: this.flags.authUser, oAuth1: this.flags.oAuth1, consumer_key: this.flags.consumer_key, - grep:this.flags.grep, + grep: this.flags.grep, optional: this.flags.optional }, - options:this.options, + options: this.options, lrsSettingsUUID: this.lrsSettingsUUID, rollupRule: this.rollupRule, @@ -291,19 +269,18 @@ class TestRunner extends EventEmitter total: this.summary.total, passed: this.summary.passed, failed: this.summary.failed, - version: version.versionNumber + version: this.xapiVersion } }; - function cleanLog(log) - { - if(!log) return undefined; + function cleanLog(log) { + if (!log) return undefined; return { title: log.title, name: log.name, requirement: log.requirement, - log:log.log, + log: log.log, status: log.status, error: log.error, tests: log.tests.map(cleanLog) diff --git a/bin/update-batteries.js b/bin/update-batteries.js new file mode 100644 index 00000000..fbff04f9 --- /dev/null +++ b/bin/update-batteries.js @@ -0,0 +1,123 @@ +const fs = require("fs"); +const path = require("path"); +const expresss = require("express"); +const TestRunner = require("./testRunner").testRunner; +const specs = require("../specConfig"); + +function createMockApp() { + let mockApp = expresss(); + + mockApp.all("/xapi/*", (req, res, next) => { + res.set('Content-Type', 'text/plain'); + res.set('x-experience-api-consistent-through', (new Date(Date.now() +100)).toISOString()); + res.set('x-experience-api-version', "1.0.3"); + res.send({ + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "version": "1.0.0", + "timestamp": (new Date(Date.now() +100)).toISOString(), + "object": { + "id": "http://www.example.com/meetings/occurances/34534", + "objectType": "Activity" + }, + "actor": { + "mbox": "mailto:xapi@adlnet.gov", + "name": "xAPI mbox", + "objectType": "Agent" + }, + "stored": (new Date(Date.now() +100)).toISOString(), + "authority": { + "mbox": "mailto:lou.wolford.ctr@adlnet.gov", + "name": "lou", + "objectType": "Agent" + }, + "id": "0be2bf9f-cb7f-4d06-9987-22a9ac406edd" + }); + }); + + return mockApp; +} + +/** + * Create a battery summary for a given spec version. + * @param {string} version + */ +async function createBattery(version) { + + const runnerFlags = {}; + runnerFlags.endpoint = "http://localhost:3001/xapi"; + runnerFlags.basicAuth = true; + runnerFlags.authUser = "No:"; + runnerFlags.authPass = "User"; + runnerFlags.xapiVersion = version; + // runnerFlags.bail = true; + + const runner = new TestRunner( + "batteryInfo", + "Admin", + runnerFlags, + null, + {} + ); + + return new Promise((resolve, reject) => { + runner.on("message", function (msg) { + if (msg.action === "end") { + function cleanLog(log) { + return { + text: log.name, + children: log.tests.map(cleanLog) + }; + } + + let record = runner.getCleanRecord(); + let info = { + conformanceTestCount: record.summary.total, + tests: cleanLog(record.log) + }; + + console.log(`[${version}] found ${record.summary.total} tests.`); + + return resolve(info); + } + }); + + runner.start(); + }); +} + +/** + * Create a battery summary for a given spec version. + * @param {string} version + */ +async function createBatteries() { + let app = createMockApp(); + let server = app.listen(3001); + + let output = {}; + for (let version of specs.availableVersions) { + + let info = await createBattery(version); + output[version] = info; + } + + server.close(); + return output; +} + +async function main() { + let batteryOutput = await createBatteries(); + let batteryPath = path.join(__dirname, "../batteries.js"); + + let fileContents = `module.exports = ${JSON.stringify(batteryOutput, null, 2)}`; + + fs.writeFileSync(batteryPath, fileContents); +} + +main(); + diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 19839bca..00000000 --- a/package-lock.json +++ /dev/null @@ -1,1707 +0,0 @@ -{ - "name": "adl-lrs-conformance-tests", - "version": "1.2.3", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "2.1.18", - "negotiator": "0.6.1" - } - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", - "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=" - }, - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "requires": { - "lodash": "4.17.5" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" - }, - "base64url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", - "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "bluebird": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-1.2.4.tgz", - "integrity": "sha1-WYXsI8tv8aWDTMZEezxe8BD9Mho=" - }, - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "1.0.4", - "debug": "2.6.9", - "depd": "1.1.2", - "http-errors": "1.6.3", - "iconv-lite": "0.4.19", - "on-finished": "2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "1.6.16" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.1" - }, - "dependencies": { - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - } - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-from": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chai": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-2.3.0.tgz", - "integrity": "sha1-ii9qNHSNqAEJD9cyh7Kqc5pOkJo=", - "requires": { - "assertion-error": "1.0.0", - "deep-eql": "0.1.3" - } - }, - "chai-as-promised": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-4.3.0.tgz", - "integrity": "sha1-D6hhsLMb/mhn9edw8Ph3vmDs5e4=" - }, - "chai-things": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chai-things/-/chai-things-0.2.0.tgz", - "integrity": "sha1-xVEoN4+bs5nplPAAUhUZhO1uvnA=" - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "colors": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.1.tgz", - "integrity": "sha512-s8+wktIuDSLffCywiwSxQOMqtPxML11a/dtHE17tMn4B1MSWw/C22EKf7M2KGUBcDaVFEGT+S8N02geDXeuNKg==" - }, - "comb": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/comb/-/comb-1.2.0.tgz", - "integrity": "sha1-77wbEhgK2jwKzVr667hwbhA8AqQ=" - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "1.0.0", - "inherits": "2.0.3", - "readable-stream": "2.3.6", - "typedarray": "0.0.6" - } - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-parser": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz", - "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=", - "requires": { - "cookie": "0.3.1", - "cookie-signature": "1.0.6" - } - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "cookiejar": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.6.tgz", - "integrity": "sha1-Cr81atANHFohnYjURRgEbdAmrP4=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.1" - } - }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - } - } - }, - "crypto-js": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz", - "integrity": "sha1-cV8HC/YBTyrpkqmLOSkli3E/CNU=" - }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "requires": { - "boolbase": "1.0.0", - "css-what": "2.1.0", - "domutils": "1.5.1", - "nth-check": "1.0.1" - } - }, - "css-what": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", - "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=" - }, - "ctype": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", - "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=", - "optional": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "requires": { - "type-detect": "0.1.1" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "diff": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.8.tgz", - "integrity": "sha1-NDJ2MI7Jkbe8giZ+1VvBQR+XFmY=" - }, - "dirty-chai": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/dirty-chai/-/dirty-chai-1.2.2.tgz", - "integrity": "sha1-eEleYZY19/5EIZqkyDeEm/GDFC4=" - }, - "dom-converter": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", - "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", - "requires": { - "utila": "0.3.3" - }, - "dependencies": { - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=" - } - } - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "requires": { - "domelementtype": "1.1.3", - "entities": "1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" - } - } - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" - }, - "domhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", - "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", - "requires": { - "domelementtype": "1.3.0" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "requires": { - "dom-serializer": "0.1.0", - "domelementtype": "1.3.0" - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", - "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", - "requires": { - "base64url": "2.0.0", - "safe-buffer": "5.1.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", - "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" - }, - "express": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", - "requires": { - "accepts": "1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "1.1.2", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.3", - "qs": "6.5.1", - "range-parser": "1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "1.4.0", - "type-is": "1.6.16", - "utils-merge": "1.0.1", - "vary": "1.1.2" - } - }, - "extend": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-2.0.1.tgz", - "integrity": "sha1-HugBBonnOV/5RIJByYZSvHWagmA=" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.4.0", - "unpipe": "1.0.0" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", - "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", - "requires": { - "async": "2.6.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - } - }, - "form-urlencoded": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/form-urlencoded/-/form-urlencoded-0.0.7.tgz", - "integrity": "sha1-+YMSD9hV76jkvDW2PNwHsYFuHv8=" - }, - "formidable": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.17.tgz", - "integrity": "sha1-71SRSQ+UM7cF+qdyScmQKa40hVk=" - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - } - }, - "glob": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", - "requires": { - "graceful-fs": "2.0.3", - "inherits": "2.0.3", - "minimatch": "0.2.14" - } - }, - "graceful-fs": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=" - }, - "growl": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.0.tgz", - "integrity": "sha512-ElsQbgadAZdhC8L+hx4RJu6fDe5uPCaRjdw8CvD39VYemGQT9CSiLdRAEYFWVIlCbPNlw+G4AbqBhQJF2qyHdg==" - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.1", - "sntp": "2.1.0" - }, - "dependencies": { - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - } - } - }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "htmlparser2": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", - "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", - "requires": { - "domelementtype": "1.3.0", - "domhandler": "2.1.0", - "domutils": "1.1.6", - "readable-stream": "1.0.34" - }, - "dependencies": { - "domutils": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", - "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", - "requires": { - "domelementtype": "1.3.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": "1.4.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.1" - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ipaddr.js": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", - "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isemail": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", - "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jade": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, - "dependencies": { - "commander": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" - }, - "mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" - } - } - }, - "joi": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", - "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", - "requires": { - "hoek": "4.2.1", - "isemail": "1.2.0", - "moment": "2.22.1", - "topo": "1.1.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonschema": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", - "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jwa": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", - "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", - "requires": { - "base64url": "2.0.0", - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.9", - "safe-buffer": "5.1.1" - } - }, - "jws": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", - "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", - "requires": { - "base64url": "2.0.0", - "jwa": "1.1.5", - "safe-buffer": "5.1.1" - } - }, - "lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" - }, - "lodash-node": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/lodash-node/-/lodash-node-3.10.2.tgz", - "integrity": "sha1-JZjVsbVOami0y1ROXHMJU8v2Mvc=" - }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "1.33.0" - } - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", - "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", - "requires": { - "minimist": "0.0.8" - } - }, - "mocha": { - "version": "1.21.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.21.5.tgz", - "integrity": "sha1-fFiwkXTfl25DSiOx6NY5hz/FKek=", - "requires": { - "commander": "2.3.0", - "debug": "2.0.0", - "diff": "1.0.8", - "escape-string-regexp": "1.0.2", - "glob": "3.2.3", - "growl": "1.8.1", - "jade": "0.26.3", - "mkdirp": "0.5.0" - }, - "dependencies": { - "commander": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", - "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=" - }, - "debug": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.0.0.tgz", - "integrity": "sha1-ib2d9nMrUSVrxnBTQrugLtEhMe8=", - "requires": { - "ms": "0.6.2" - } - }, - "growl": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.0.tgz", - "integrity": "sha1-Sy3sjZB+k9szZiTc7AGDUC+MlCg=" - }, - "ms": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", - "integrity": "sha1-2JwhJMb9wTU9Zai3e/GqxLGTcIw=" - } - } - }, - "moment": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", - "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "node-env-file": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/node-env-file/-/node-env-file-0.1.3.tgz", - "integrity": "sha1-CjkjALxk23ehcbYsDoGhot67wbw=", - "requires": { - "chai": "4.1.2" - }, - "dependencies": { - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" - }, - "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", - "requires": { - "assertion-error": "1.1.0", - "check-error": "1.0.2", - "deep-eql": "3.0.1", - "get-func-name": "2.0.0", - "pathval": "1.1.0", - "type-detect": "4.0.8" - } - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "requires": { - "type-detect": "4.0.8" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - } - } - }, - "node-uuid": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", - "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" - }, - "nth-check": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", - "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", - "requires": { - "boolbase": "1.0.0" - } - }, - "oauth": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" - }, - "oauth-1.0a": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-1.0.1.tgz", - "integrity": "sha1-Kam1gNfPXeKVc+Imn7Bes++x2Bk=", - "requires": { - "crypto-js": "3.1.8" - } - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "open": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", - "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pretty-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", - "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", - "requires": { - "renderkid": "2.0.1", - "utila": "0.4.0" - } - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "proxy-addr": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", - "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", - "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.6.0" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "questions": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/questions/-/questions-0.0.6.tgz", - "integrity": "sha1-ee0UZBdoC5xnrwJefEq1RqmQntk=" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.4.0" - } - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "reduce-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/reduce-component/-/reduce-component-1.0.1.tgz", - "integrity": "sha1-4Mk1QsV0UhvqE98PlIjtgqt3xdo=" - }, - "renderkid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", - "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", - "requires": { - "css-select": "1.2.0", - "dom-converter": "0.1.4", - "htmlparser2": "3.3.0", - "strip-ansi": "3.0.1", - "utila": "0.3.3" - }, - "dependencies": { - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=" - } - } - }, - "request": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - }, - "dependencies": { - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - } - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" - } - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "1.1.2", - "destroy": "1.0.4", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "fresh": "0.5.2", - "http-errors": "1.6.3", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.4.0" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "should": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/should/-/should-4.6.5.tgz", - "integrity": "sha1-DRI0bbvRsCj59Lt6nVRzZPw2qH8=", - "requires": { - "should-equal": "0.3.1", - "should-format": "0.0.7", - "should-type": "0.0.4" - } - }, - "should-equal": { - "version": "0.3.1", - "resolved": "http://registry.npmjs.org/should-equal/-/should-equal-0.3.1.tgz", - "integrity": "sha1-vY6pemdI45+tR2o75v1y68LnK/A=", - "requires": { - "should-type": "0.0.4" - } - }, - "should-format": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/should-format/-/should-format-0.0.7.tgz", - "integrity": "sha1-Hi74a9kdqcLgQSM1tWq6vZov3hI=", - "requires": { - "should-type": "0.0.4" - } - }, - "should-type": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/should-type/-/should-type-0.0.4.tgz", - "integrity": "sha1-ATKgVBemEmhmQmrPEW8e1WI6XNA=" - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.1" - }, - "dependencies": { - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - } - } - }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - } - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - }, - "string": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/string/-/string-3.3.3.tgz", - "integrity": "sha1-XqIRzZLSKOGEKUmQpsyXs2anfLA=" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "super-request": { - "version": "file:local/super-request", - "requires": { - "comb": "0.3.6", - "methods": "0.1.0", - "request": "2.34.0" - }, - "dependencies": { - "asn1": { - "version": "0.1.11", - "bundled": true, - "optional": true - }, - "assert-plus": { - "version": "0.1.5", - "bundled": true, - "optional": true - }, - "async": { - "version": "0.9.2", - "bundled": true, - "optional": true - }, - "aws-sign2": { - "version": "0.5.0", - "bundled": true, - "optional": true - }, - "boom": { - "version": "0.4.2", - "bundled": true, - "requires": { - "hoek": "4.2.1" - } - }, - "comb": { - "version": "0.3.6", - "bundled": true - }, - "combined-stream": { - "version": "0.0.7", - "bundled": true, - "optional": true, - "requires": { - "delayed-stream": "0.0.5" - } - }, - "cryptiles": { - "version": "0.2.2", - "bundled": true, - "optional": true, - "requires": { - "boom": "0.4.2" - } - }, - "delayed-stream": { - "version": "0.0.5", - "bundled": true, - "optional": true - }, - "forever-agent": { - "version": "0.5.2", - "bundled": true - }, - "form-data": { - "version": "0.1.4", - "bundled": true, - "optional": true, - "requires": { - "async": "0.9.2", - "combined-stream": "0.0.7", - "mime": "1.2.11" - } - }, - "hawk": { - "version": "1.0.0", - "bundled": true, - "optional": true, - "requires": { - "boom": "0.4.2", - "cryptiles": "0.2.2", - "hoek": "4.2.1", - "sntp": "0.2.4" - } - }, - "hoek": { - "version": "4.2.1", - "bundled": true - }, - "http-signature": { - "version": "0.10.1", - "bundled": true, - "optional": true, - "requires": { - "asn1": "0.1.11", - "assert-plus": "0.1.5", - "ctype": "0.5.3" - } - }, - "methods": { - "version": "0.1.0", - "bundled": true - }, - "mime": { - "version": "1.2.11", - "bundled": true - }, - "oauth-sign": { - "version": "0.3.0", - "bundled": true, - "optional": true - }, - "qs": { - "version": "0.6.6", - "bundled": true - }, - "request": { - "version": "2.34.0", - "bundled": true, - "requires": { - "aws-sign2": "0.5.0", - "forever-agent": "0.5.2", - "form-data": "0.1.4", - "hawk": "1.0.0", - "http-signature": "0.10.1", - "json-stringify-safe": "5.0.1", - "mime": "1.2.11", - "node-uuid": "1.4.8", - "oauth-sign": "0.3.0", - "qs": "0.6.6", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.3.0" - } - }, - "sntp": { - "version": "0.2.4", - "bundled": true, - "optional": true, - "requires": { - "hoek": "4.2.1" - } - }, - "tunnel-agent": { - "version": "0.3.0", - "bundled": true, - "optional": true - } - } - }, - "superagent": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-1.8.5.tgz", - "integrity": "sha1-HA3cOvMOgOuE68BcshItqP6UC1U=", - "requires": { - "component-emitter": "1.2.1", - "cookiejar": "2.0.6", - "debug": "2.6.9", - "extend": "3.0.0", - "form-data": "1.0.0-rc3", - "formidable": "1.0.17", - "methods": "1.1.2", - "mime": "1.3.4", - "qs": "2.3.3", - "readable-stream": "1.0.27-1", - "reduce-component": "1.0.1" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", - "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=" - }, - "form-data": { - "version": "1.0.0-rc3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc3.tgz", - "integrity": "sha1-01vGLn+8KTeuePlIqqDTjZBgdXc=", - "requires": { - "async": "1.5.2", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", - "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" - }, - "qs": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", - "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=" - }, - "readable-stream": { - "version": "1.0.27-1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.27-1.tgz", - "integrity": "sha1-a2eYPCA1fO/QfwFlABoW1xDZEHg=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "superagent-oauth": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/superagent-oauth/-/superagent-oauth-0.2.3.tgz", - "integrity": "sha1-UA+GXC4TgXJMgPtnlpugSTrYV2M=" - }, - "supertest": { - "version": "file:local/supertest", - "requires": { - "methods": "1.1.2", - "superagent": "1.8.5" - } - }, - "supertest-as-promised": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supertest-as-promised/-/supertest-as-promised-1.0.0.tgz", - "integrity": "sha1-c13NNwWhcIXbI131HYGJU8H7t6w=", - "requires": { - "bluebird": "1.2.4", - "methods": "1.1.2" - } - }, - "topo": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", - "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", - "requires": { - "hoek": "4.2.1" - } - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=" - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "2.1.18" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" - }, - "validator": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-5.7.0.tgz", - "integrity": "sha1-eoelgUa2laxIYHEUHAxJ1n2gXlw=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - } - } -} diff --git a/package.json b/package.json index 109af8a8..207fd6f4 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,34 @@ { "name": "adl-lrs-conformance-tests", - "version": "1.2.3", + "version": "2.0.0-0", "description": "lrs-conformance-tests", "main": "./bin/console_runner.js", "scripts": { - "test": "node_modules/mocha/bin/mocha test/v1_0_2/*.js --reporter nyan", - "start": "node ./bin/console_runner.js" + "test": "node node_modules/mocha/bin/mocha test/v1_0_2/*.js --reporter nyan", + "start": "node ./bin/console_runner.js", + "update-batteries": "node ./bin/update-batteries.js" }, "repository": { "type": "git", - "url": "git://github.com/TryxAPI/lrs-conformance-tests.git" + "url": "git://github.com/adlnet/lrs-conformance-test-suite.git" }, "author": "", "license": "MIT", "bugs": { - "url": "https://github.com/TryxAPI/lrs-conformance-tests/issues" + "url": "https://github.com/adlnet/lrs-conformance-test-suite/issues" }, "bin": { "lrs-test": "./bin/console_runner.js" }, - "homepage": "https://github.com/TryxAPI/lrs-conformance-tests", + "homepage": "https://github.com/adlnet/lrs-conformance-test-suite", "dependencies": { + "axios": "^1.4.0", + "axios-oauth-1.0a": "^0.3.6", "chai": "^2.3.0", - "chai-as-promised": "^4.1.1", "chai-things": "^0.2.0", "colors": "^1.1.2", "comb": "^1.0.1", "commander": "^2.6.0", - "concat-stream": "^1.5.1", "cookie-parser": "^1.4.1", "dirty-chai": "^1.0.0", "exit": "^0.1.2", @@ -35,23 +36,18 @@ "extend": "^2.0.0", "form-data": "^1.0.0-rc4", "form-urlencoded": "0.0.7", - "growl": "^1.10.0", "isemail": "^1.1.1", "joi": "^6.4.1", "jsonschema": "^1.1.0", "jws": "^3.1.3", - "lodash-node": "^3.2.0", + "lodash.isequal": "^4.5.0", "mocha": "^1.20.1", "moment": "^2.8.4", "node-env-file": "0.1.3", - "node-uuid": "^1.4.1", "oauth": "^0.9.14", - "oauth-1.0a": "^1.0.1", "open": "0.0.5", "pretty-error": "^2.0.0", "q": "^1.1.2", - "querystring": "^0.2.0", - "questions": "0.0.6", "request": "^2.37.0", "should": "^4.0.4", "string": "^3.1.0", @@ -59,7 +55,7 @@ "superagent-oauth": "^0.2.3", "supertest": "file:./local/supertest", "supertest-as-promised": "^1.0.0", - "uuid": "^2.0.1", + "uuid": "^9.0.0", "validator": "^5.7.0" } } diff --git a/specConfig.js b/specConfig.js new file mode 100644 index 00000000..c71e0380 --- /dev/null +++ b/specConfig.js @@ -0,0 +1,36 @@ +const specInfo = { + + defaultVersion: "2.0.0", + availableVersions: [ + "1.0.3", + "2.0.0" + ], + + specToFolder: { + "1.0.2": "v1_0_2", + "1.0.3": "v1_0_3", + + "2": "v2_0", + "2.0": "v2_0", + "2.0.0": "v2_0", + }, + + /** + * Get a version from the given folder. + * @param {string} folder + */ + getSpecFromFolder(folder) { + if (folder.includes("v1_0_3")) + return "1.0.3"; + + if (folder.includes("v1_0_2")) + return "1.0.2"; + + if (folder.includes("v2_0")) + return "2.0.0"; + + return null; + } +} + +module.exports = specInfo; diff --git a/test/helper.js b/test/helper.js index cffb53ac..4f7a9a96 100644 --- a/test/helper.js +++ b/test/helper.js @@ -9,7 +9,7 @@ var path = require('path'); if (!process.env.EB_NODE_COMMAND) { (require('node-env-file'))(path.join(__dirname, './.env')); } -(function (module, fs, extend, uuid, lodash, FormUrlencode, jws, crypto) { +(function (module, fs, extend, uuid, lodashIsEqual, FormUrlencode, jws, crypto) { var oauth; @@ -69,8 +69,8 @@ if (!process.env.EB_NODE_COMMAND) { /** Endpoint Statements */ var URL_STATEMENTS = '/statements'; - /** HTTP header xAPI Version */ - var XAPI_VERSION = '1.0.3'; + // /** HTTP header xAPI Version */ + // var XAPI_VERSION = '2.0.0'; module.exports = { /** @@ -596,7 +596,7 @@ if (!process.env.EB_NODE_COMMAND) { * @returns {String} */ getXapiVersion: function() { - return XAPI_VERSION; + return process.env.XAPI_VERSION; }, /** * Performs deep compare of JSON original / other parameters to determine equivalence. @@ -605,7 +605,7 @@ if (!process.env.EB_NODE_COMMAND) { * @returns {boolean} */ isEqual: function (original, other) { - return lodash.isEqual(original, other); + return lodashIsEqual(original, other); }, /** * Returns a string of a form encoded content with headers. @@ -614,7 +614,7 @@ if (!process.env.EB_NODE_COMMAND) { */ buildFormBody: function (content, id) { var body = { - 'X-Experience-API-Version': '1.0.3', + 'X-Experience-API-Version': process.env.XAPI_VERSION, 'content': JSON.stringify(content) } if (id) { @@ -624,7 +624,6 @@ if (!process.env.EB_NODE_COMMAND) { }, /** * Returns an example Activity params. - * @returns {string} */ buildActivity: function () { return { @@ -633,7 +632,6 @@ if (!process.env.EB_NODE_COMMAND) { }, /** * Returns an example State params. - * @returns {json} state */ buildState: function () { return { @@ -650,7 +648,6 @@ if (!process.env.EB_NODE_COMMAND) { }, /** * Returns an example ActivityProfile params. - * @returns {json} activity profile */ buildActivityProfile: function () { return { @@ -660,7 +657,6 @@ if (!process.env.EB_NODE_COMMAND) { }, /** * Returns an example AgentProfile. - * @returns {json} agent profile */ buildAgentProfile: function () { return { @@ -676,7 +672,6 @@ if (!process.env.EB_NODE_COMMAND) { }, /** * Returns an example Agent params. - * @returns {json} agent */ buildAgent: function () { return { @@ -692,7 +687,6 @@ if (!process.env.EB_NODE_COMMAND) { }, /** * Return sample document. - * @returns {json} document */ buildDocument: function () { var document = { @@ -990,4 +984,4 @@ if (!process.env.EB_NODE_COMMAND) { } }); } -}(module, require('fs'), require('extend'), require('node-uuid'), require('lodash-node'), require('form-urlencoded'), require('jws'), require('crypto'))); +}(module, require('fs'), require('extend'), require('uuid'), require('lodash.isequal'), require('form-urlencoded'), require('jws'), require('crypto'))); diff --git a/test/templatingSelection.js b/test/templatingSelection.js index a8f90f6a..a16759be 100644 --- a/test/templatingSelection.js +++ b/test/templatingSelection.js @@ -35,9 +35,9 @@ try { var data = {}; if (test.templates) { - // convert template mapping to JSON objects + // Convert template mapping to JSON objects. var converted = helper.convertTemplate(test.templates); - // this handles if no override + // This handles if no override. var mockObject = helper.createTestObject(converted); var key = Object.keys(mockObject); data = mockObject[key]; diff --git a/test/v1_0_3/H.Communication2.8-AboutResource.js b/test/v1_0_3/H.Communication2.8-AboutResource.js index eafc5d81..230c4804 100644 --- a/test/v1_0_3/H.Communication2.8-AboutResource.js +++ b/test/v1_0_3/H.Communication2.8-AboutResource.js @@ -78,10 +78,12 @@ describe('About Resource Requirements (Communication 2.8)', function () { .then(function (res) { var about = res.body; expect(about).to.have.property('version').to.be.an('array'); - var validVersions = ['0.9', '0.95', '1.0.0', '1.0.1', '1.0.2', '1.0.3']; - about.version.forEach(function (item) { - expect(validVersions).to.include(item); - }) + // var validVersions = ['0.9', '0.95', '1.0.0', '1.0.1', '1.0.2', '1.0.3']; + // about.version.forEach(function (item) { + // expect(validVersions).to.include(item); + // }); + + expect(about.version).to.include("1.0.3"); }); }); @@ -101,7 +103,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected expect(res.statusCode).to.eql(400); expect(res.headers['x-experience-api-version']).to.exist; - expect(res.headers['x-experience-api-version']).to.match(/^1\.0\.\d+$|^0?\.9\d*?$/); + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); done(); } else if (res.statusCode === 200) { // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers @@ -117,7 +119,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { } else { str += 'missing'; } - str += '.\nExpected: either 400 with LRS version 1.0.x, or 200 with LRS version 0.9x.'; + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; done(new Error(str)); } }); @@ -143,7 +145,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected expect(res.statusCode).to.eql(400); expect(res.headers['x-experience-api-version']).to.exist; - expect(res.headers['x-experience-api-version']).to.match(/^1\.0\.\d+$|^0?\.9\d*?$/); + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); done(); } else if (res.statusCode === 200) { // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers @@ -159,7 +161,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { } else { str += 'missing'; } - str += '.\nExpected: either 400 with LRS version 1.0.x, or 200 with LRS version 0.9x.'; + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; done(new Error(str)); } }); @@ -176,7 +178,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected expect(res.statusCode).to.eql(400); expect(res.headers['x-experience-api-version']).to.exist; - expect(res.headers['x-experience-api-version']).to.match(/^1\.0\.\d+$|^0?\.9\d*?$/); + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); done(); } else if (res.statusCode === 200) { // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers @@ -192,7 +194,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { } else { str += 'missing'; } - str += '.\nExpected: either 400 with LRS version 1.0.x, or 200 with LRS version 0.9x.'; + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; done(new Error(str)); } }); @@ -209,7 +211,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected expect(res.statusCode).to.eql(400); expect(res.headers['x-experience-api-version']).to.exist; - expect(res.headers['x-experience-api-version']).to.match(/^1\.0\.\d+$|^0?\.9\d*?$/); + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); done(); } else if (res.statusCode === 200) { // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers @@ -225,7 +227,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { } else { str += 'missing'; } - str += '.\nExpected: either 400 with LRS version 1.0.x, or 200 with LRS version 0.9x.'; + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; done(new Error(str)); } }); @@ -242,7 +244,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected expect(res.statusCode).to.eql(400); expect(res.headers['x-experience-api-version']).to.exist; - expect(res.headers['x-experience-api-version']).to.match(/^1\.0\.\d+$|^0?\.9\d*?$/); + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); done(); } else if (res.statusCode === 200) { // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers @@ -258,7 +260,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { } else { str += 'missing'; } - str += '.\nExpected: either 400 with LRS version 1.0.x, or 200 with LRS version 0.9x.'; + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; done(new Error(str)); } }); @@ -275,7 +277,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected expect(res.statusCode).to.eql(400); expect(res.headers['x-experience-api-version']).to.exist; - expect(res.headers['x-experience-api-version']).to.match(/^1\.0\.\d+$|^0?\.9\d*?$/); + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); done(); } else if (res.statusCode === 200) { // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers @@ -291,7 +293,7 @@ describe('About Resource Requirements (Communication 2.8)', function () { } else { str += 'missing'; } - str += '.\nExpected: either 400 with LRS version 1.0.x, or 200 with LRS version 0.9x.'; + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; done(new Error(str)); } }); diff --git a/test/v2_0/4.1.3-Content-Types.js b/test/v2_0/4.1.3-Content-Types.js new file mode 100644 index 00000000..2c3d4a3d --- /dev/null +++ b/test/v2_0/4.1.3-Content-Types.js @@ -0,0 +1,712 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +const request = require('super-request'); +const fs = require("fs"); +const crypto = require("crypto"); +const expect = require("chai").expect; +const helper = require("../helper"); +const xapiRequests = require("./util/requests"); + +if (global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Content Type Requirements (Communication 1.5)', function () { + var txtAtt1, txtAtt2, txtAtt3, + t1attSize, t2attSize, t3attSize, + t1attHash, t2attHash, t3attHash; + + before('create attachments templates', function () { + txtAtt1 = fs.readFileSync('test/v1_0_3/templates/attachments/simple_text1.txt'); + txtAtt2 = fs.readFileSync('test/v1_0_3/templates/attachments/simple_text2.txt'); + txtAtt3 = fs.readFileSync('test/v1_0_3/templates/attachments/simple_text3.txt'); + + var t1stats = fs.statSync('test/v1_0_3/templates/attachments/simple_text1.txt'); + var t2stats = fs.statSync('test/v1_0_3/templates/attachments/simple_text2.txt'); + var t3stats = fs.statSync('test/v1_0_3/templates/attachments/simple_text3.txt'); + t1attSize = t1stats.size; + t2attSize = t2stats.size; + t3attSize = t3stats.size; + t1attHash = crypto.createHash('SHA256').update(txtAtt1).digest('hex'); + t2attHash = crypto.createHash('SHA256').update(txtAtt2).digest('hex'); + t3attHash = crypto.createHash('SHA256').update(txtAtt3).digest('hex'); + }); + + //Communication 1.5.1 + /** Matchup with Conformance Requirements Document + * XAPI-00127 - below + * XAPI-00128 - below + * XAPI-00129 - below + */ + + //Communication 1.5.2 + /** + * XAPI-00130 - below + * XAPI-00131 - below + * XAPI-00132 - below + * XAPI-00133 - below + * XAPI-00134 - below + * XAPI-00135 - below + * there is no XAPI-00136 + * XAPI-00137 - removed + * XAPI-00138 - removed + */ + + + /** XAPI-00127, Communication 1.5.1 Application/JSON + * An LRS rejects with error code 400 Bad Request, a PUT or POST Request which does not have a "Content-Type" header with value "application/json" or "multipart/mixed" + */ + describe('An LRS rejects with error code 400 Bad Request, a Request which uses Attachments and does not have a "Content-Type" header with value "application/json" or "multipart/mixed" (Format, Data 2.4.11, XAPI-00127)', function () { + var data, pictureAtt, pattSize, pattHash, templates; + + before('create attachment templates', function () { + templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain; charset=ascii", + "length": 27, + "sha2": "495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a", + "fileUrl": "http://over.there.com/file.txt" + } + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + pictureAtt = fs.readFileSync('test/v1_0_3/templates/attachments/basic_image_p2.jpeg', { encoding: 'hex' }); + var pstats = fs.statSync('test/v1_0_3/templates/attachments/basic_image_p2.jpeg'); + pattSize = pstats.size; + pattHash = crypto.createHash('SHA256').update(pictureAtt).digest('hex'); + }); + + it('should succeed when attachment uses "fileUrl" and request content-type is "application/json"', function (done) { + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data).expect(200, done); + }); + + it('should succeed when attachment uses "fileUrl" and request content-type is "application/json"', function (done) { + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data).expect(200, done); + }); + + it('should fail when attachment uses "fileUrl" and request content-type is "multipart/form-data"', function (done) { + var header = { 'Content-Type': 'multipart/form-data; boundary=-------314159265358979323846' }; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(JSON.stringify(data)).expect(400, done); + }); + + it('should succeed when attachment is raw data and request content-type is "multipart/mixed"', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + + delete data.attachments[0].fileUrl; + data.attachments[0].contentType = 'image/jpeg'; + data.attachments[0].length = pattSize; + data.attachments[0].sha2 = pattHash; + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: image/jpeg' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += pictureAtt + crlf; + msg += dashes + boundary + dashes + crlf; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(200, done); + }); + + it('should fail when attachment is raw data and request content-type is "multipart/form-data"', function (done) { + var header = { 'Content-Type': 'multipart/form-data; boundary=-------314159265358979323846' }; + + delete data.attachments[0].fileUrl; + data.attachments[0].contentType = 'image/jpeg'; + data.attachments[0].length = pattSize; + data.attachments[0].sha2 = pattHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: image/jpeg' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += pictureAtt + crlf; + msg += dashes + boundary + dashes + crlf; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + + it('should succeed when attachment uses "fileUrl" and request content-type is "multipart/mixed"', function (done) { + let boundary = '-------314159265358979323846' + let header = { 'Content-Type': 'multipart/mixed; boundary=' + boundary }; + + let data = helper.createFromTemplate(templates); + let statement = data.statement; + + let dashes = '--'; + let crlf = '\r\n'; + + let msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(statement) + crlf; + msg += dashes + boundary + dashes + crlf; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(200, done); + }); + + it('should succeed when no attachments are included, but request content-type is "multipart/mixed"', function (done) { + let boundary = '-------314159265358979323846' + let header = { 'Content-Type': 'multipart/mixed; boundary=' + boundary }; + + delete data.attachments; + + let dashes = '--'; + let crlf = '\r\n'; + + let msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + dashes + crlf; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(200, done); + }); + }); + + /** XAPI-00128, Communication 1.5.1 Application/JSON + * An LRS rejects with error code 400 Bad Request, a PUT or POST Request which has excess multi-part sections that are not attachments. + */ + describe('An LRS rejects with error code 400 Bad Request, a PUT or POST Request which has excess multi-part sections that are not attachments. (Communication 1.5.1.s1.b2, Data 2.4.11, XAPI-00128)', function () { + it('should fail when passing statement attachments with excess multipart sections', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 33, + "sha2": "" + } + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + data.attachments[1].length = t2attSize; + data.attachments[1].sha2 = t2attHash; + + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846'; + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[1].sha2 + crlf + crlf; + msg += txtAtt2 + crlf; + + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + t3attHash + crlf + crlf; + msg += txtAtt3 + crlf; + + msg += dashes + boundary + dashes + crlf; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + }); + + /** XAPI-00129, Communication 1.5.1 Application/JSON + * An LRS rejects with error code 400 Bad Request, a PUT or POST Request which is missing multi-part sections for non-fileURL attachments must be rejected. + */ + describe('An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content-Type" header with value "application/json", and has a discrepancy in the number of Attachments vs. the number of fileURL members (Communication 1.5.1.s1.b2, Data 2.4.11, XAPI-00129)', function () { + it('should fail when passing statement attachments and missing attachment"s binary', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 33, + "sha2": "" + } + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + data.attachments[1].length = t2attSize; + data.attachments[1].sha2 = t2attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + }); + + /** XAPI-00131, Communication 1.5.2 Multipart/Mixed + * An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and does not have a body header named "boundary" + */ + describe('An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and does not have a body header named "boundary" (Communication 1.5.2.s2.b2, Data 2.4.11, RFC 2046, XAPI-00131)', function () { + it('should fail if boundary not provided in body', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + }); + + /** XAPI-00130, Communication 1.5.2 Multipart/Mixed + * An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and does not have a Boundary before each "Content-Type" header + */ + describe('An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and does not have a Boundary before each "Content-Type" header (Communication 1.5.2.s2.b2, Data 2.4.11, RFC 2046, XAPI-00130)', function () { + it('should fail if boundary not provided in body', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + + it('should fail if boundary not provided in header', function (done) { + var header = { 'Content-Type': 'multipart/mixed;' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + }); + + /** XAPI-00134, Communication 1.5.2 Multipart/Mixed + * An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and does not the first document part with a "Content-Type" header with a value of "application/json" + */ + describe('An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and does not the first document part with a "Content-Type" header with a value of "application/json" (RFC 2046, Communication 1.5.2.s2.b2.b1, Data 2.4.11, XAPI-00134)', function () { + it('should fail when attachment is raw data and first part content type is not "application/json"', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + }); + + /** XAPI-00133, Communication 1.5.2 Multipart/Mixed + * An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and does not have all of the Statements in the first document part + */ + describe('An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and does not have all of the Statements in the first document part (RFC 2046, Data 2.4.11, Communication 1.5.2.s2.b2.b1, XAPI-00133)', function () { + it('should fail when statements separated into multiple parts', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + }); + + /** XAPI-00132, Communication 1.5.2 Multipart/Mixed + * An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and for any part except the first does not have a Header named "X-Experience-API-Hash" with a value of one of those found in a "sha2" property of a Statement in the first part of this document + */ + describe('An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and for any part except the first does not have a Header named "X-Experience-API-Hash" with a value of one of those found in a "sha2" property of a Statement in the first part of this document (Communication 1.5.2.s2.b2.b3", Communication 1.5.2.s1.b4, Data 2.4.11, XAPI-00132)', function () { + it('should fail when attachments missing header "X-Experience-API-Hash"', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + + it('should fail when attachments header "X-Experience-API-Hash" does not match "sha2"', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + 'b018994f8bbe0f08992a65c48c8c8c56f09e9baceaa6227ed85c90ae52b73c89' + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); + }); + + /** XAPI-00135, Communication 1.5.2 Multipart/Mixed + * An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and for any part except the first does not have a Header named "Content-Transfer-Encoding" with a value of "binary" + */ + it('An LRS rejects with error code 400 Bad Request, a PUT or POST Request which uses Attachments, has a "Content Type" header with value "multipart/mixed", and for any part except the first does not have a Header named "Content-Transfer-Encoding" with a value of "binary" (Data 2.4.11, XAPI-00135)', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 27, + "sha2": "" + }, + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: base64' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg).expect(400, done); + }); +}); diff --git a/test/v2_0/4.1.4-Concurrency.js b/test/v2_0/4.1.4-Concurrency.js new file mode 100644 index 00000000..e30e9fae --- /dev/null +++ b/test/v2_0/4.1.4-Concurrency.js @@ -0,0 +1,262 @@ +'use strict'; +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +var request = require('supertest-as-promised'); +const expect = require('chai').expect; +const helper = require('../helper'); +const xapiRequests = require("./util/requests"); + +request = request(helper.getEndpoint()); + +function runConcurrencyTestsForDocumentResource(resourceName, resourcePath, resourceParams) { + + describe(`Concurrency for the ${resourceName} Resource.`, () => { + + let document = helper.buildDocument(); + + before('before', async() => { + await xapiRequests.deleteDocument(resourcePath, resourceParams); + await xapiRequests.postDocument(resourcePath, document, resourceParams); + }); + + it('An LRS responding to a GET request SHALL add an ETag HTTP header to the response.', async() => { + + let documentResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + let etag = documentResponse.headers.etag; + + expect(etag).to.be.a("string"); + }); + + it('When responding to a GET Request the Etag header must be enclosed in quotes', async() => { + + let documentResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + let etag = documentResponse.headers.etag; + + expect(etag).to.be.a("string"); + + if (etag[0] !== '"') { + expect(etag[0]).to.equal('W'); + expect(etag[1]).to.equal('/'); + etag = str.substring(2) + } + + expect(etag[0]).to.equal('"'); + expect(etag[41]).to.equal('"'); + }); + + describe('When responding to a PUT, POST, or DELETE request, must handle the If-Match header as described in RFC 2616, HTTP/1.1 if it contains an ETag', async () => { + + describe("Properly handles PUT requests with If-Match", async() => { + + let document = helper.buildDocument(); + let originalName = document.name; + let updatedDocument = { + ...document, + name: "Updated Name:" + helper.generateUUID() + } + var correctTag; + + before ("Get the current ETag", async() => { + + await xapiRequests.deleteDocument(resourcePath, resourceParams); + await xapiRequests.postDocument(resourcePath, document, resourceParams); + + let documentResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + correctTag = documentResponse.headers.etag; + }); + + it("Should reject a PUT request with a 412 Precondition Failed when using an incorrect ETag", async() => { + + let incorrectTag = "1234"; + let incorrectResponse = await xapiRequests.putDocument(resourcePath, document, resourceParams, { "If-Match": incorrectTag }); + + expect(incorrectResponse.status).to.equal(412); + }); + + it("Should not have modified the document for PUT requests with an incorrect ETag", async() => { + + let originalDocResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + expect(originalDocResponse.data.name).to.equal(originalName); + }); + + it("Should accept a PUT request with a correct ETag", async() => { + + let correctResponse = await xapiRequests.putDocument(resourcePath, updatedDocument, resourceParams, { "If-Match": correctTag }); + expect(correctResponse.status).to.equal(204); + }); + + it("Should have modified the document for PUT requests with a correct ETag", async() => { + + let updatedResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + expect(updatedResponse.data).to.eql(updatedDocument); + }); + }); + + describe("Properly handles POST requests with If-Match", function() { + + let document = helper.buildDocument(); + let originalName = document.name; + let updatedDocument = { + ...document, + name: "Updated Name:" + helper.generateUUID() + } + var correctTag; + + before ("Get the current ETag", async() => { + + await xapiRequests.deleteDocument(resourcePath, resourceParams); + await xapiRequests.postDocument(resourcePath, document, resourceParams); + + let documentResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + correctTag = documentResponse.headers.etag; + }); + + it("Should reject a POST request with a 412 Precondition Failed when using an incorrect ETag", async() => { + + let incorrectTag = "1234"; + let incorrectResponse = await xapiRequests.postDocument(resourcePath, document, resourceParams, { "If-Match": incorrectTag }); + + expect(incorrectResponse.status).to.equal(412); + }); + + it("Should not have modified the document for POST requests with an incorrect ETag", async() => { + + let originalDocResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + expect(originalDocResponse.data.name).to.equal(originalName); + }); + + it("Should accept a POST request with a correct ETag", async() => { + + let correctResponse = await xapiRequests.postDocument(resourcePath, updatedDocument, resourceParams, { "If-Match": correctTag }); + expect(correctResponse.status).to.equal(204); + }); + + it("Should have modified the document for POST requests with a correct ETag", async() => { + + let updatedResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + expect(updatedResponse.data).to.eql(updatedDocument); + }); + }); + + describe ("Properly handles DELETE requests with If-Match", function() { + + let document = helper.buildDocument(); + let originalName = document.name; + var correctTag; + + before ("Get the current ETag", async() => { + + await xapiRequests.deleteDocument(resourcePath, resourceParams); + await xapiRequests.postDocument(resourcePath, document, resourceParams); + + let documentResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + correctTag = documentResponse.headers.etag; + }); + + it("Should reject a DELETE request with a 412 Precondition Failed when using an incorrect ETag", async() => { + + let incorrectTag = "1234"; + let incorrectResponse = await xapiRequests.deleteDocument(resourcePath, resourceParams, { "If-Match": incorrectTag }); + + expect(incorrectResponse.status).to.equal(412); + }); + + it("Should not have modified the document for DELETE requests with an incorrect ETag", async() => { + + let originalDocResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + + expect(originalDocResponse.status).to.equal(200); + expect(originalDocResponse.data.name).to.equal(originalName); + }); + + it("Should accept a DELETE request with a correct ETag", async() => { + + let correctResponse = await xapiRequests.deleteDocument(resourcePath, resourceParams, { "If-Match": correctTag }); + expect(correctResponse.status).to.equal(204); + }); + + /** + * Note, the LRS isn't actually required to return a 404. The language is currently: + * + * 404 Not Found - Indicates the requested resource was not found. + * May be returned by any method that returns a uniquely identified resource, + * for instance, any State, Agent Profile, or Activity Profile Resource request + * targeting a specific document, or the method to retrieve a single Statement. + * + * Even though this is arguably the most appropriate error code for this situation, + * which --is-- a requirement: + * + * An LRS shall return the error code most appropriate to the error condition from the list above. + */ + }); + }); + + /** + * Presently, If-None-Match is not included in the spec document for xAPI 2.0's LRS requirements. + * + * This seems to be a presumed inclusion, but it will be omitted here until the document explicitly + * requires its implementation by the LRS. + */ + + describe('If a PUT request is received without either header for a resource that already exists', function () { + var etag; + var originalDocument = helper.buildDocument(); + var updatedDocument = helper.buildDocument(); + + before('Create the document and get the etag', async () => { + + await xapiRequests.deleteDocument(resourcePath, resourceParams); + let postResponse = await xapiRequests.postDocument(resourcePath, originalDocument, resourceParams); + + expect(postResponse.status).to.equal(204); + + let getResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + + expect(getResponse.status).to.equal(200); + expect(getResponse.headers.etag).to.not.be.undefined; + expect(getResponse.data).to.eql(originalDocument); + + etag = getResponse.headers.etag; + }); + + it('Return 409 conflict', async () => { + let res = await xapiRequests.putDocument(resourcePath, updatedDocument, resourceParams); + expect(res.status).to.equal(409); + }); + + it('Return error message explaining the situation', async () => { + let res = await xapiRequests.putDocument(resourcePath, updatedDocument, resourceParams); + let responseText = res.data; + + expect(responseText).is.a("string").with.length.greaterThan(0); + }); + + it('Do not modify the resource', async () => { + let getResponse = await xapiRequests.getDocuments(resourcePath, resourceParams); + + expect(getResponse.data).to.eql(originalDocument); + }); + }); + }); +} + +describe('(4.1.4) Concurrency', () => { + + /** XAPI-00322, Communication 3.1 Concurrency + * An LRS must support HTTP/1.1 entity tags (ETags) to implement optimistic concurrency control when handling APIs where PUT may overwrite existing data (State, Agent Profile, and Activity Profile) + */ + describe("xAPI uses HTTP 1.1 entity tags (ETags) to implement optimistic concurrency control in the following resources, where PUT, POST or DELETE are allowed to overwrite or remove existing data.", function() { + + let stateParams = helper.buildState(); + let activityProfileParams = helper.buildActivityProfile(); + let agentsProfileParams = helper.buildAgentProfile(); + + runConcurrencyTestsForDocumentResource("Activity State", xapiRequests.resourcePaths.activityState, stateParams); + runConcurrencyTestsForDocumentResource("Activity Profile", xapiRequests.resourcePaths.activityProfile, activityProfileParams); + runConcurrencyTestsForDocumentResource("Agents Profile", xapiRequests.resourcePaths.agentsProfile, agentsProfileParams); + }); +}); + diff --git a/test/v2_0/4.1.6.1-Statement-Resource.js b/test/v2_0/4.1.6.1-Statement-Resource.js new file mode 100644 index 00000000..0a7fd98b --- /dev/null +++ b/test/v2_0/4.1.6.1-Statement-Resource.js @@ -0,0 +1,3395 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +const request = require('super-request'); +const extend = require("extend"); +const fs = require("fs"); +const expect = require("chai").expect; +const helper = require("../helper"); +const xapiRequests = require("./util/requests"); +const multipartParser = require("../multipartParser"); +const crypto = require("crypto"); +const moment = require("moment"); + +if (global.OAUTH) + request = helper.OAuthRequest(request); + +//Communication 2.0 +/** Matchup with Conformance Requirements Document + * XAPI-00139 - below + * XAPI-00140 - generic and covered by other files - An LRS implements all of the Statement, State, Agent, and Activity Profile sub-APIs + * XAPI-00141 - covered by XAPI-00195, XAPI-00275, XAPI-00294 + */ +describe('Statement Resource Requirements (Communication 2.1)', () => { + + /** XAPI-00139, Communication 2.0 Resources + * An LRS has a Statement API with endpoint "base IRI"+"/statements" + */ + describe('An LRS has a Statement Resource with endpoint "base IRI"+"/statements" (Communication 2.1, XAPI-00139)', function () { + it('should allow "/statements" POST', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200, done); + }); + + it('should allow "/statements" PUT', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('should allow "/statements" GET', function (done) { + var query = helper.getUrlEncoding({ verb: 'http://adlnet.gov/expapi/non/existent' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + //Communication 2.1.1 PUT Statements + /** Matchup with Conformance Requirements Document + * XAPI-00142 - below + * XAPI-00143 - below + * XAPI-00144 - below + * XAPI-00145 - below + */ + + + /** XAPI-00143, Communication 2.1.1 PUT Statements + * An LRS's Statement API upon processing a valid PUT request successfully returns code 204 No Content + */ + describe('An LRS\'s Statement Resource upon processing a successful PUT request returns code 204 No Content (Communication 2.1.1.s1, XAPI-00143)', function () { + it('should persist statement and return status 204', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + }); + + /** XAPI-00144, Communication 2.1.1 PUT Statements + * An LRS's Statement API accepts PUT requests only if it contains a "statementId" parameter, returning 204 No Content + */ + /** XAPI-00145, Communication 2.1.1 PUT Statements + * An LRS's Statement API rejects a PUT request which does not have a "statementId" parameter, returning 400 Bad Request + */ + describe('An LRS\'s Statement Resource accepts PUT requests only if it contains a "statementId" parameter (Multiplicity, Communication 2.1.1.s1.table1.row1, XAPI-00144, XAPI-00145)', function () { + it('should persist statement using "statementId" parameter', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('should fail without using "statementId" parameter', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(400, done); + }); + }); + + /** XAPI-00142, Communication 2.1.1 PUT Statements + * An LRS cannot modify a Statement in the event it receives a Statement with statementID equal to a Statement in the LRS already. To test: Send one statement with a particular statement ID. Send a second statement with the same statement ID but everything else different. Retrieve the statement before the second statement and after and both retrieved statements MUST match. + */ + describe('An LRS cannot modify a Statement, state, or Object in the event it receives a Statement with statementID equal to a Statement in the LRS already. (Communication 2.1.1.s2.b2, XAPI-00142)', function () { + this.timeout(0); + it('should not update statement with matching "statementId" on PUT', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var query = '?statementId=' + data.id; + + var modified = extend(true, {}, data); + modified.verb.id = 'http://example.com/different/verb/iri'; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(modified) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?statementId=' + data.id) + .wait(helper.genDelay(stmtTime, query, data.id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var statement = helper.parse(res.body, done); + expect(statement.verb.id).to.equal(data.verb.id); + done(); + } + }); + } + }); + } + }); + }); + + it('should not update statement with matching "statementId" on POST', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var query = '?statementId=' + data.id; + var modified = extend(true, {}, data); + modified.verb.id = 'http://example.com/different/verb/iri'; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(modified) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?statementId=' + data.id) + .wait(helper.genDelay(stmtTime, query, data.id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var statement = helper.parse(res.body, done); + expect(statement.verb.id).to.equal(data.verb.id); + done(); + } + }); + } + }); + } + }); + }); + + it('should reject a batch of two or more statements where the same ID is used more than once.', function (done) { + + let statementOne = helper.buildStatement(); + let statementTwo = JSON.parse(JSON.stringify(statementOne)); + + let id = helper.generateUUID(); + statementOne.id = id; + statementTwo.id = id; + + let payload = [statementOne, statementTwo]; + + xapiRequests.sendStatementPromise(payload) + .then(res => { + expect(res.status).to.eql(400); + done(); + }) + .catch(err => { + expect(err.response.status).to.eql(400); + done(); + }); + }); + + it('should include a Last-Modified header which matches the "stored" Timestamp of the statement.', function (done) { + + let statement = helper.buildStatement(); + xapiRequests.sendStatementPromise(statement).then(postResponse => { + + let storedId = postResponse.data[0]; + xapiRequests.getStatementExact(storedId).then(getResponse => { + + let lastModifiedStr = getResponse.headers.get("last-modified"); + let lastModified = Date.parse(lastModifiedStr); + expect(lastModified).to.not.eql(Number.NaN, + `The Last-Modified header could not be parsed -- received: ${lastModifiedStr}` + ); + + let retrievedStatement = getResponse.data; + let storedStr = retrievedStatement.stored; + let stored = Date.parse(storedStr); + expect(stored).to.not.eql(Number.NaN, + `The "stored" property could not be parsed into a DateTime, received: ${storedStr}` + ); + + let storedWithoutMS = stored - (stored % 1000); + let lastModifiedWithoutMS = lastModified - (lastModified % 1000); + + expect(storedWithoutMS).to.eql(lastModifiedWithoutMS, + `The "stored" property did not match the Last-Modified to the seconds value: ${storedStr} vs. ${lastModifiedStr}` + ); + + done(); + }); + }); + }); + }); + + //Communication 2.1.2 POST Statements + /** Matchup with Conformance Requirements Document + * XAPI-00146 - below + * XAPI-00147 - below + * XAPI-00148 - in H.Communication1.3-AlternateRequestSyntax.js + */ + + /** XAPI-00147, Communication 2.1.2 POST Statements + * An LRS's Statement API accepts POST requests + */ + describe('An LRS\'s Statement Resource accepts POST requests (Communication 2.1.2.s1, XAPI-00147)', function () { + it('should persist statement using "POST"', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200, done); + }); + }); + + /** XAPI-00146, Communication 2.1.2 POST Statements + * An LRS's Statement API upon processing a successful POST request returns code 200 OK and all Statement UUIDs within the POST + */ + describe('An LRS\'s Statement Resource upon processing a successful POST request returns code 200 OK and all Statement UUIDs within the POST **Implicit** (Communication 2.1.2.s1, XAPI-00146)', function () { + it('should persist statement using "POST" and return array of IDs', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID() + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err) + } else { + expect(res.body).to.be.an('array').to.have.length.above(0); + done(); + } + }); + }); + }); + + //Communication 2.1.3 GET Statements + /** Matchup with Conformance Requirements Document + * XAPI-00149 - below + * XAPI-00150 - below + * XAPI-00151 - below + * XAPI-00152 - removed per spec call 2/8/17 + * XAPI-00153 - below + * XAPI-00154 - below + * XAPI-00155 - below + * XAPI-00156 - below + * XAPI-00157 - below + * XAPI-00158 - below + * XAPI-00159 - below + * XAPI-00160 - below + * XAPI-00161 - below + * XAPI-00162 - below + * XAPI-00163 - below + * XAPI-00164 - below + * XAPI-00165 - below + * XAPI-00166 - below + * XAPI-00167 - below + * XAPI-00168 - below + * XAPI-00169 - below + * XAPI-00170 - below + * XAPI-00171 - below + * XAPI-00172 - below + * XAPI-00173 - below + * XAPI-00174 - below + * XAPI-00175 - below + * XAPI-00176 - below + * XAPI-00177 - below + * XAPI-00178 - below + * XAPI-00179 - below + * XAPI-00180 - below + * XAPI-00181 - below + */ + + /** XAPI-00159, Communication 2.1.3 GET Statements + * An LRS's Statement API accepts GET requests + */ + describe('LRS\'s Statement Resource accepts GET requests (Communication 2.1.3.s1, XAPI-00159)', function () { + it('should return using GET', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00156, Communication 2.1.3 GET Statements + * An LRS's Statement API upon processing a successful GET request with a "statementId" parameter, returns code 200 OK and a single Statement with the corresponding "id". + */ + describe('An LRS\'s Statement Resource upon processing a successful GET request with a "statementId" parameter, returns code 200 OK and a single Statement with the corresponding "id". (Communication 2.1.3.s1, XAPI-00156)', function () { + var id, stmtTime; + + before('persist statement', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + id = data.id; + + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200, done); + }); + + it('should retrieve statement using "statementId"', function (done) { + this.timeout(0); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?statementId=' + id) + .wait(helper.genDelay(stmtTime, '?statementId=' + id, id)) + .headers(helper.addAllHeaders({})) + .expect(200).end(function (err, res) { + if (err) { + done(err); + } else { + var statement = helper.parse(res.body, done); + expect(statement.id).to.equal(id); + done(); + } + }); + }); + }); + + /** XAPI-00155, Communication 2.1.3 GET Statements + * An LRS's Statement API upon processing a successful GET request with a + "voidedStatementId" parameter, returns code 200 OK and a single Statement with the corresponding "id". + */ + describe('An LRS\'s Statement Resource upon processing a successful GET request with a "voidedStatementId" parameter, returns code 200 OK and a single Statement with the corresponding "id". (Communication 2.1.3.s1, XAPI-00155)', function () { + var voidedId = helper.generateUUID(); + var stmtTime; + + before('persist voided statement', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var voided = helper.createFromTemplate(templates); + voided = voided.statement; + voided.id = voidedId; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voided) + .expect(200, done); + }); + + before('persist voiding statement', function (done) { + var templates = [ + { statement: '{{statements.voiding}}' } + ]; + var voiding = helper.createFromTemplate(templates); + voiding = voiding.statement; + voiding.object.id = voidedId; + + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voiding) + .expect(200, done); + }); + + it('should return a voided statement when using GET "voidedStatementId"', function (done) { + this.timeout(0); + var query = helper.getUrlEncoding({ voidedStatementId: voidedId }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var statement = helper.parse(res.body, done); + expect(statement.id).to.equal(voidedId); + done(); + } + }); + }); + }); + + /** XAPI-00154, Communication 2.1.3 GET Statements + * An LRS's Statement API upon processing a successful GET request with neither a "statementId" nor a "voidedStatementId" parameter, returns code 200 OK and a + StatementResult Object. + */ + describe('An LRS\'s Statement Resource upon processing a successful GET request with neither a "statementId" nor a "voidedStatementId" parameter, returns code 200 OK and a StatementResult Object. (Communication 2.1.3.s1, XAPI-00154)', function () { + var statement, substatement, stmtTime; + this.timeout(0); + + before('persist statement', function (done) { + var templates = [ + { statement: '{{statements.context}}' }, + { context: '{{contexts.category}}' }, + { + instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:pri@adlnet.gov" + } + } + ]; + var data = helper.createFromTemplate(templates); + statement = data.statement; + statement.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/pri'; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(statement) + .expect(200, done); + }); + + before('persist substatement', function (done) { + var templates = [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: '{{contexts.category}}' }, + { + instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:sub@adlnet.gov" + } + } + ]; + var data = helper.createFromTemplate(templates); + substatement = data.statement; + substatement.object.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/sub'; + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(substatement) + .expect(200, done); + }); + + it('should return StatementResult using GET without "statementId" or "voidedStatementId"', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .wait(helper.genDelay(stmtTime, undefined, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "agent"', function (done) { + var templates = [ + { agent: '{{agents.default}}' } + ]; + var data = helper.createFromTemplate(templates); + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "verb"', function (done) { + var query = helper.getUrlEncoding({ verb: statement.verb.id }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "activity"', function (done) { + var query = helper.getUrlEncoding({ activity: statement.object.id }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "registration"', function (done) { + var query = helper.getUrlEncoding({ registration: statement.context.registration }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "related_activities"', function (done) { + var query = helper.getUrlEncoding({ + activity: statement.context.contextActivities.category.id, + related_activities: true + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "related_agents"', function (done) { + var query = helper.getUrlEncoding({ + agent: statement.context.instructor, + related_agents: true + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "since"', function (done) { + var query = helper.getUrlEncoding({ since: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "until"', function (done) { + var query = helper.getUrlEncoding({ until: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "limit"', function (done) { + var query = helper.getUrlEncoding({ limit: 1 }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "ascending"', function (done) { + var query = helper.getUrlEncoding({ ascending: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult using GET with "format"', function (done) { + var query = helper.getUrlEncoding({ format: 'ids' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var results = helper.parse(res.body, done); + expect(results).to.have.property('statements'); + done(); + } + }); + }); + }); + + /** XAPI-00158, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "statementId" as a parameter + */ + describe('An LRS\'s Statement Resource can process a GET request with "statementId" as a parameter (Communication 2.1.3.s1.table1.row1, XAPI-00158)', function () { + it('should process using GET with "statementId"', function (done) { + this.timeout(0); + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var query = '?statementId=' + data.id; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, data.id)) + .headers(helper.addAllHeaders({})) + .expect(200, done); + } + }); + }); + }); + + /** XAPI-00157, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "voidedStatementId" as a parameter + */ + describe('An LRS\'s Statement Resource can process a GET request with "voidedStatementId" as a parameter (Communication 2.1.3.s1.table1.row2, XAPI-00157)', function () { + var voidedId = helper.generateUUID(); + var stmtTime; + + before('persist voided statement', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var voided = helper.createFromTemplate(templates); + voided = voided.statement; + voided.id = voidedId; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voided) + .expect(200, done); + }); + + before('persist voiding statement', function (done) { + var templates = [ + { statement: '{{statements.voiding}}' } + ]; + var voiding = helper.createFromTemplate(templates); + voiding = voiding.statement; + voiding.object.id = voidedId; + stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voiding) + .expect(200, done); + }); + + it('should process using GET with "voidedStatementId"', function (done) { + this.timeout(0); + var query = helper.getUrlEncoding({ voidedStatementId: voidedId }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00181, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "agent" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with exact match agent result if the agent parameter is set with a valid Agent IFI + */ + describe('An LRS\'s Statement Resource can process a GET request with "agent" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row3, XAPI-00181)', function () { + it('should process using GET with "agent"', function (done) { + var templates = [ + { agent: '{{agents.default}}' } + ]; + var data = helper.createFromTemplate(templates); + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00180, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "verb" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with exact match verb results if the verb parameter is set with a valid Verb IRI + */ + describe('An LRS\'s Statement Resource can process a GET request with "verb" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row4, XAPI-00180)', function () { + it('should process using GET with "verb"', function (done) { + var query = helper.getUrlEncoding({ verb: 'http://adlnet.gov/expapi/non/existent' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00179, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "activity" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with exact match activity results if the activity parameter is set with a valid activity IRI + */ + describe('An LRS\'s Statement Resource can process a GET request with "activity" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row5, XAPI-00179)', function () { + it('should process using GET with "activity"', function (done) { + var query = helper.getUrlEncoding({ activity: 'http://www.example.com/meetings/occurances/12345' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00178, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "registration" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with exact match registration results if the registration parameter is set with a valid registration UUID + */ + describe('An LRS\'s Statement Resource can process a GET request with "registration" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row6, XAPI-00178)', function () { + it('should process using GET with "registration"', function (done) { + var query = helper.getUrlEncoding({ registration: helper.generateUUID() }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00177, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "related_activities" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with exact match activity results if the activity parameter is set with a valid Verb IRI unless the related_activities parameter is set to true. If set to true it MUST return 200 OK, StatementResult Object with activity ID matches in the Statement Object, and Context Objects and SubStatement Objects. + */ + describe('An LRS\'s Statement Resource can process a GET request with "related_activities" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row7)', function () { + var statement, stmtTime; + + before('persist statement', function (done) { + var templates = [ + { statement: '{{statements.context}}' }, + { context: '{{contexts.category}}' }, + { + instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:pri@adlnet.gov" + } + } + ]; + var data = helper.createFromTemplate(templates); + statement = data.statement; + statement.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/pri'; + stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(statement) + .expect(200, done); + }); + + it('should process using GET with "related_activities"', function (done) { + this.timeout(0); + var query = helper.getUrlEncoding({ + activity: statement.context.contextActivities.category.id, + related_activities: true + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00176. Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "related_agents" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with exact match agent results if the agent parameter is set with a valid Agent or Identified Group JSON Object unless the related_agents parameter is set to true. If set to true it MUST return 200 OK, StatementResult Object with agent matches in the Actor, Object, authority, instructor, team, or any of these properties in a contained SubStatement + */ + describe('An LRS\'s Statement Resource can process a GET request with "related_agents" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row8, XAPI-00176)', function () { + var statement, stmtTime; + + before('persist statement', function (done) { + var templates = [ + { statement: '{{statements.context}}' }, + { context: '{{contexts.category}}' }, + { + instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:pri@adlnet.gov" + } + } + ]; + var data = helper.createFromTemplate(templates); + statement = data.statement; + statement.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/pri'; + stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(statement) + .expect(200, done); + }); + + it('should process using GET with "related_agents"', function (done) { + this.timeout(0); + + var query = helper.getUrlEncoding({ + agent: statement.context.instructor, + related_agents: true + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00175, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "since" as a parameter. The Statement API MUST return 200 OK, StatementResult Object containing all statements which have a stored timestamp after the since parameter timestamp in the query. + */ + describe('An LRS\'s Statement Resource can process a GET request with "since" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row9, XAPI-00175)', function () { + it('should process using GET with "since"', function (done) { + var query = helper.getUrlEncoding({ since: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00174, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "until" as a parameter. The Statement API MUST return 200 OK, StatementResult Object containing all statements which have a stored timestamp at or before the specified until parameter timestamp. + */ + describe('An LRS\'s Statement Resource can process a GET request with "until" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row10, XAPI-00174)', function () { + it('should process using GET with "until"', function (done) { + var query = helper.getUrlEncoding({ until: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00173, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "limit" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with only the number of results set by the integer in the limit parameter. If the limit parameter is not present, the limit is defaulted to 0 which returns all results up to the server limit. + */ + describe('An LRS\'s Statement Resource can process a GET request with "limit" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row11, XAPI-00173)', function () { + it('should process using GET with "limit"', function (done) { + var query = helper.getUrlEncoding({ limit: 1 }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00172, Communication 2.1.3 GET Statements + * If the "Accept-Language" header is present as part of the GET request to the Statement API and the "format" parameter is set to "canonical", the LRS MUST apply this data to choose the matching language in the response. + */ + describe('If the "Accept-Language" header is present as part of the GET request to the Statement API and the "format" parameter is set to "canonical", the LRS MUST apply this data to choose the matching language in the response. (Communication 2.1.3.s1.table1.row11, XAPI-00172)', function () { + var statement; + var statementID; + var stmtTime; + before('persist statement', function (done) { + var templates = [ + { statement: '{{statements.context}}' }, + { context: '{{contexts.category}}' }, + { + instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:pri@adlnet.gov" + } + } + ]; + var data = helper.createFromTemplate(templates); + statement = data.statement; + statement.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/pri'; + + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(statement) + .expect(200, function (err, res) { + statementID = res.body[0]; + done(err); + }); + }); + + it('should apply this data to choose the matching language in the response', function (done) { + this.timeout(0); + var query = helper.getUrlEncoding({ + statementId: statementID, + format: "canonical" + }); + + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(null, null, statementID)) + .headers(helper.addAllHeaders({ "Accept-Language": "en-GB" })) + .expect(200, function (err, res) { + if (err) console.log(err); + + var statement = JSON.parse(res.body); + // console.log(require("util").inspect(statement,{depth:7})); + expect(statement.verb.display).not.to.have.property("en-US"); + expect(statement.context.contextActivities.category[0].definition.description).not.to.have.property("en-US"); + expect(statement.context.contextActivities.category[0].definition.name).not.to.have.property("en-US"); + done(err); + }); + }); + + it('should NOT apply this data to choose the matching language in the response when format is not set ', function (done) { + this.timeout(0); + var query = helper.getUrlEncoding({ + statementId: statementID, + }); + + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(null, null, statementID)) + .headers(helper.addAllHeaders({ "Accept-Language": "en-GB" })) + .expect(200, function (err, res) { + if (err) console.log(err); + + var statement = JSON.parse(res.body); + // console.log(require("util").inspect(statement,{depth:7})); + expect(statement.verb.display).to.have.property("en-US"); + expect(statement.context.contextActivities.category[0].definition.description).to.have.property("en-US"); + expect(statement.context.contextActivities.category[0].definition.name).to.have.property("en-US"); + + expect(statement.verb.display).to.have.property("en-GB"); + expect(statement.context.contextActivities.category[0].definition.description).to.have.property("en-GB"); + expect(statement.context.contextActivities.category[0].definition.name).to.have.property("en-GB"); + done(err); + }); + }); + + }) + + /** XAPI-00168, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "format" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with results in the requested format or in “exact” if the “format” parameter is absent. + */ + /** XAPI-00169, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "format" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with results in the requested format. If “canonical”, return Activity Objects and Verbs populated with the canonical definition of the Activity Objects and Display of the Verbs as determined by the LRS, returning only one language. + */ + /** XAPI-00170, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "format" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with results in the requested format. If “exact”, return Agent, Activity, Verb and Group Objects populated exactly as they were when the Statement was received. + */ + /** XAPI-00171, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "format" as a parameter. The Statement API MUST return 200 OK, StatementResult Object with results in the requested format. If “ids”, only include identifiers for Agent, Activity, Verb, Group Objects, and members of Anonymous groups. + */ + describe('An LRS\'s Statement Resource can process a GET request with "format" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row12)', function () { + this.timeout(0); + var agent, activity, group, verb1, verb2, id, stmtTime; + before('setting up the statement to test against', function (done) { + var templates = [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{statements.unicode}}' }, + { actor: '{{groups.default}}' } + ]; + var data = helper.createFromTemplate(templates).statement; + agent = data.actor; + agent.mbox = 'mailto:agent' + helper.generateUUID() + '@adlnet.gov'; + verb1 = data.verb; + group = data.object.actor; + group.mbox = 'mailto:group' + helper.generateUUID() + '@adlnet.gov'; + verb2 = data.object.verb; + data.object.object.id = 'http://www.example.com/unicode/' + helper.generateUUID(); + activity = data.object.object; + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + id = res.body[0]; + done(); + } + }) + }); + // XAPI-00168 + it('should process using GET with "format" absent (XAPI-00168)', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .wait(helper.genDelay(stmtTime, '?statementId=' + id, id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + var stmts = result.statements; + expect(stmts).to.be.an('array'); + stmts.forEach(function (stmt) { + if (stmt.id === id) { + expect(stmt.actor).to.eql(agent); + expect(stmt.verb).to.eql(verb1); + expect(stmt.object.actor).to.eql(group); + expect(stmt.object.object).to.eql(activity); + expect(stmt.object.verb).to.eql(verb2); + } + }); + done(); + } + }); + }); + // XAPI-00169 + it('should process using GET with "format" canonical (XAPI-00169)', function (done) { + var query = helper.getUrlEncoding({ format: 'canonical' }); + + // Build a better actor + var canonicalActor = {}; + canonicalActor.mbox = agent.mbox; + canonicalActor.objectType = agent.objectType; + canonicalActor.name = agent.name; + + // Build a better verb + var mainVerb = {}; + mainVerb.id = verb1.id; + mainVerb.display = {}; + mainVerb.display["en-GB"] = verb1.display["en-GB"]; + + // Build a better substatement verb + var subVerb = {}; + subVerb.id = verb2.id; + subVerb.display = {}; + subVerb.display["en-GB"] = verb2.display["en-GB"]; + + // Build a better activity + var canonicalSubActivity = {}; + canonicalSubActivity.objectType = activity.objectType; + canonicalSubActivity.id = activity.id; + canonicalSubActivity.definition = {}; + canonicalSubActivity.definition.name = {} + canonicalSubActivity.definition.name["en-GB"] = activity.definition.name["en-GB"]; + canonicalSubActivity.definition.description = {}; + canonicalSubActivity.definition.description["en-GB"] = activity.definition.description["en-GB"]; + canonicalSubActivity.definition.type = activity.definition.type; + canonicalSubActivity.definition.moreInfo = activity.definition.moreInfo; + canonicalSubActivity.definition.interactionType = activity.definition.interactionType; + canonicalSubActivity.definition.correctResponsesPattern = activity.definition.correctResponsesPattern; + canonicalSubActivity.definition.extensions = activity.definition.extensions; + + // Build a better group + var canonicalGroup = {}; + canonicalGroup.mbox = group.mbox; + canonicalGroup.objectType = group.objectType; + canonicalGroup.name = group.name; + + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({ "Accept-Language": "en-GB" })) + .wait(helper.genDelay(stmtTime, '?statementId=' + id, id)) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + var stmts = result.statements; + expect(stmts).to.be.an('array'); + stmts.forEach(function (stmt) { + if (stmt.id === id) { + expect(stmt.actor).to.eql(canonicalActor); + expect(stmt.verb).to.eql(mainVerb); + expect(stmt.object.verb).to.eql(subVerb); + expect(stmt.object.object).to.eql(canonicalSubActivity); + expect(stmt.object.actor).to.eql(canonicalGroup); + } + }); + done(); + } + }); + }); + // XAPI-00170 + it('should process using GET with "format" exact (XAPI-00170)', function (done) { + var query = helper.getUrlEncoding({ format: 'exact' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?statementId=' + id, id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + var stmts = result.statements; + expect(stmts).to.be.an('array'); + stmts.forEach(function (stmt) { + if (stmt.id === id) { + expect(stmt.actor).to.eql(agent); + expect(stmt.verb).to.eql(verb1); + expect(stmt.object.actor).to.eql(group); + expect(stmt.object.verb).to.eql(verb2); + expect(stmt.object.object).to.eql(activity); + } + }); + done(); + } + }); + }); + // XAPI-00171 + it('should process using GET with "format" ids (XAPI-00171)', function (done) { + var query = helper.getUrlEncoding({ format: 'ids' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?statementId=' + id, id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + var stmts = result.statements; + expect(stmts).to.be.an('array'); + stmts.forEach(function (stmt) { + if (stmt.id === id) { + expect(Object.keys(stmt.actor).length).to.be.within(1, 2); + expect(Object.keys(stmt.object.actor).length).to.eql(2); + expect(Object.keys(stmt.object.object).length).to.eql(1); + + /** Re-adding these as it's once again a requirement for 2.0 */ + expect(Object.keys(stmt.verb).length).to.eql(1); + expect(Object.keys(stmt.object.verb).length).to.eql(1); + /** Re-adding these as it's once again a requirement for 2.0 */ + + expect(stmt.actor.mbox).to.eql(agent.mbox); + if (stmt.actor.objectType) { + expect(stmt.actor.objectType).to.eql(agent.objectType); + } + expect(stmt.verb.id).to.eql(verb1.id); + expect(stmt.object.actor.mbox).to.eql(group.mbox); + expect(stmt.object.actor.objectType).to.eql(group.objectType); + expect(stmt.object.object.id).to.eql(activity.id); + expect(stmt.object.verb.id).to.eql(verb2.id); + } + }); + done(); + } + }); + }); + }); + + /** XAPI-00167, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "attachments" as a parameter. The Statement API MUST return 200 OK, StatementResult Object and use the multipart response format and include all attachments if the attachment parameter is set to true + */ + describe('An LRS\'s Statement Resource can process a GET request with "attachments" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row13, XAPI-00167)', function () { + + var stmtTime, stmtId; + + before('set up statement with two attachments for test', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "contentType": "text/plain", + "length": 0, + "sha2": "", + "description": { "en-US": "A test attachment (description)" } + }, + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "contentType": "text/plain", + "length": 0, + "sha2": "", + "description": { "en-US": "A test attachment (description)" } + } + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + txtAtt1 = fs.readFileSync('test/v1_0_3/templates/attachments/simple_text1.txt'); + var t1stats = fs.statSync('test/v1_0_3/templates/attachments/simple_text1.txt'); + t1attSize = t1stats.size; + t1attHash = crypto.createHash('SHA256').update(txtAtt1).digest('hex'); + + txtAtt2 = fs.readFileSync('test/v1_0_3/templates/attachments/simple_text2.txt'); + var t2stats = fs.statSync('test/v1_0_3/templates/attachments/simple_text2.txt'); + t2attSize = t2stats.size; + t2attHash = crypto.createHash('SHA256').update(txtAtt2).digest('hex'); + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + data.attachments[1].length = t2attSize; + data.attachments[1].sha2 = t2attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[1].sha2 + crlf + crlf; + msg += txtAtt2 + crlf; + msg += dashes + boundary + dashes + crlf; + + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg) + .expect(200, function (err, res) { + if (err) { + done(err); + } else { + stmtId = helper.parse(res.body, done)[0]; + done(); + } + }); + }); + + it('should return multipart response format StatementResult using GET with "attachments" parameter as true', function (done) { + var query = helper.getUrlEncoding({ attachments: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + expect(res.headers).to.have.property('content-type'); + var boundary = multipartParser.getBoundary(res.headers['content-type']); + expect(boundary).to.be.ok; + var parsed = multipartParser.parseMultipart(boundary, res.body); + expect(parsed).to.be.ok; + var results = helper.parse(parsed[0].body, done); + expect(results).to.have.property('statements'); + done(); + } + }); + }); + + it('should not return multipart response format using GET with "attachments" parameter as false', function (done) { + var query = helper.getUrlEncoding({ attachments: false }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var results = helper.parse(res.body); + expect(results).to.have.property('statements'); + done(); + } + }); + }); + + it('should process using GET with "attachments"', function (done) { + this.timeout(0); + var query = helper.getUrlEncoding({ attachments: true, statementId: stmtId }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, stmtId)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err); + } else { + expect(res.headers['content-type']).to.include('multipart/mixed'); + // Find the boundary + var b = res.headers['content-type'].split(';'); + var boundary; + var quotes = b[1].match(/"/g); + if (quotes) { + boundary = b[1].trim().match(/"([^"]+)"/)[1]; + } else { + var temp = b[1].trim(); + boundary = temp.substring(temp.indexOf('=') + 1); + } + // Verify we have the statement we asked for + // Use boundary to get the first part of response, excluding "--" + var x = res.body.split(boundary); + var c = x[1].substring(x[1].indexOf('{'), x[1].lastIndexOf('}') + 1); + var result = helper.parse(c, done); + expect(result).to.have.property('id'); + expect(result.id).to.equal(stmtId); + // Create an array of global matches of the pattern, the length of which is equal to the number of times that pattern appears in the given string + var regex1 = new RegExp(t1attHash, 'g'); + var regex2 = new RegExp(t2attHash, 'g'); + var match1 = (res.body.match(regex1) || []).length; + var match2 = (res.body.match(regex2) || []).length; + // Compare that number to 2 the number of times it is expected for a given has to appear in the response, once in the attachments property, and once along with the attachment + expect(match1).to.eql(2); + expect(match2).to.eql(2); + done(); + } + }); + }); + }); + + /** XAPI-00165, Communication 2.1.3 GET Statements + * An LRS's Statement API, upon receiving a GET request, + MUST have a "Content-Type" header + */ + describe('An LRSs Statement Resource, upon receiving a GET request, MUST have a "Content-Type" header(**Implicit**, Communication 2.1.3.s1.table1.row14, XAPI-00165)', function () { + it('should contain the content-type header', function (done) { + var query = helper.getUrlEncoding({ ascending: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + expect(res.headers).to.have.property("content-type"); + done(); + + }); + }); + }); + + + /** XAPI-00166, Communication 2.1.3 GET Statements + * An LRS's Statement API can process a GET request with "ascending" as a parameter The Statement API MUST return 200 OK, StatementResult Object with results in ascending order of stored time if the ascending parameter is set to true. + */ + describe('An LRS\'s Statement Resource can process a GET request with "ascending" as a parameter (**Implicit**, Communication 2.1.3.s1.table1.row14, XAPI-00166)', function () { + it('should process using GET with "ascending"', function (done) { + var query = helper.getUrlEncoding({ ascending: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00151, Communication 2.1.3 GET Statements + * An LRS's Statement API rejects a GET request with both "statementId" and anything other than "attachments" or "format" as parameters with error code 400 Bad Request. + */ + describe('An LRS\'s Statement Resource rejects with error code 400 a GET request with both "statementId" and anything other than "attachments" or "format" as parameters (Communication 2.1.3.s2.b2, XAPI-00151)', function () { + var id; + var stmtTime; + this.timeout(0); + + before('persist statement', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + id = data.id; + stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200, done); + }); + + it('should fail when using "statementId" with "agent"', function (done) { + var templates = [ + { agent: '{{agents.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data.statementId = id; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "statementId" with "verb"', function (done) { + var data = { + statementId: id, + verb: 'http://adlnet.gov/expapi/non/existent' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "statementId" with "activity"', function (done) { + var data = { + statementId: id, + activity: 'http://www.example.com/meetings/occurances/12345' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "statementId" with "registration"', function (done) { + var data = { + statementId: id, + registration: helper.generateUUID() + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "statementId" with "related_activities"', function (done) { + var data = { + statementId: id, + related_activities: true + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "statementId" with "related_agents"', function (done) { + var data = { + statementId: id, + related_agents: true + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "statementId" with "since"', function (done) { + var data = { + statementId: id, + since: '2012-06-01T19:09:13.245Z' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "statementId" with "until"', function (done) { + var data = { + statementId: id, + until: '2012-06-01T19:09:13.245Z' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "statementId" with "limit"', function (done) { + var data = { + statementId: id, + limit: 1 + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "statementId" with "ascending"', function (done) { + var data = { + statementId: id, + ascending: true + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should pass when using "statementId" with "format"', function (done) { + var data = { + statementId: id, + format: 'ids' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + + it('should pass when using "statementId" with "attachments"', function (done) { + var data = { + statementId: id, + attachments: true + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00150, Communication 2.1.3 GET Statements + * An LRS's Statement API rejects a GET request with both "voidedStatementId" and anything other than "attachments" or "format" as parameters with error code 400 Bad Request. + */ + describe('An LRS\'s Statement Resource rejects with error code 400 a GET request with both "voidedStatementId" and anything other than "attachments" or "format" as parameters (Communication 2.1.3.s2.b2, XAPI-00150)', function () { + var voidedId = helper.generateUUID(); + var stmtTime; + this.timeout(0); + + before('persist voided statement', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var voided = helper.createFromTemplate(templates); + voided = voided.statement; + voided.id = voidedId; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voided) + .expect(200, done); + }); + + before('persist voiding statement', function (done) { + var templates = [ + { statement: '{{statements.voiding}}' } + ]; + var voiding = helper.createFromTemplate(templates); + voiding = voiding.statement; + voiding.object.id = voidedId; + + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voiding) + .expect(200, done); + }); + + it('should fail when using "voidedStatementId" with "agent"', function (done) { + var templates = [ + { agent: '{{agents.default}}' } + ]; + var data = helper.createFromTemplate(templates); + data.statementId = voidedId; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "voidedStatementId" with "verb"', function (done) { + var data = { + statementId: voidedId, + verb: 'http://adlnet.gov/expapi/non/existent' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "voidedStatementId" with "activity"', function (done) { + var data = { + statementId: voidedId, + activity: 'http://www.example.com/meetings/occurances/12345' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "voidedStatementId" with "registration"', function (done) { + var data = { + statementId: voidedId, + registration: helper.generateUUID() + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "voidedStatementId" with "related_activities"', function (done) { + var data = { + statementId: voidedId, + related_activities: true + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "voidedStatementId" with "related_agents"', function (done) { + var data = { + statementId: voidedId, + related_agents: true + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "voidedStatementId" with "since"', function (done) { + var data = { + statementId: voidedId, + since: '2012-06-01T19:09:13.245Z' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "voidedStatementId" with "until"', function (done) { + var data = { + statementId: voidedId, + until: '2012-06-01T19:09:13.245Z' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "voidedStatementId" with "limit"', function (done) { + var data = { + statementId: voidedId, + limit: 1 + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail when using "voidedStatementId" with "ascending"', function (done) { + var data = { + statementId: voidedId, + ascending: true + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should pass when using "voidedStatementId" with "format"', function (done) { + var data = { + voidedStatementId: voidedId, + format: 'ids' + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + + it('should pass when using "voidedStatementId" with "attachments"', function (done) { + var data = { + voidedStatementId: voidedId, + attachments: true + }; + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + }); + + /** XAPI-00149, Communication 2.1.3 GET Statements + * The LRS will NOT reject a GET request which returns an empty "statements" property. Send a GET request which will not return any results and check that a 200 Ok and an empty StatementResult Object is returned. + */ + describe('The LRS will NOT reject a GET request which returns an empty "statements" property (**Implicit**, Communication 2.1.3.s2.b4, XAPI-00149)', function () { + it('should return empty array list', function (done) { + var query = helper.getUrlEncoding({ verb: 'http://adlnet.gov/expapi/non/existent' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err) + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.be.length(0); + done(); + } + }); + }); + }); + + /** XAPI-00153, Communication 2.1.3 GET Statements + * An LRS's Statement API upon processing a GET request, returns a header with name "X-Experience-API-Consistent-Through" regardless of the code returned. + */ + describe('An LRS\'s Statement Resource upon processing a GET request, returns a header with name "X-Experience-API-Consistent-Through" regardless of the code returned. (Communication 2.1.3.s2.b5, XAPI-00153)', function () { + it('should return "X-Experience-API-Consistent-Through" using GET', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" misusing GET (status code 400)', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?LIMIT=1') + .headers(helper.addAllHeaders({})) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "agent"', function (done) { + var templates = [ + { agent: '{{agents.default}}' } + ]; + var data = helper.createFromTemplate(templates); + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "verb"', function (done) { + var query = helper.getUrlEncoding({ verb: 'http://adlnet.gov/expapi/non/existent' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "activity"', function (done) { + var query = helper.getUrlEncoding({ activity: 'http://www.example.com/meetings/occurances/12345' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "registration"', function (done) { + var query = helper.getUrlEncoding({ registration: helper.generateUUID() }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "related_activities"', function (done) { + var query = helper.getUrlEncoding({ related_activities: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "related_agents"', function (done) { + var query = helper.getUrlEncoding({ related_agents: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "since"', function (done) { + var query = helper.getUrlEncoding({ since: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "until"', function (done) { + var query = helper.getUrlEncoding({ until: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "limit"', function (done) { + var query = helper.getUrlEncoding({ limit: 1 }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "ascending"', function (done) { + var query = helper.getUrlEncoding({ ascending: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "format"', function (done) { + var query = helper.getUrlEncoding({ format: 'ids' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "attachments"', function (done) { + var query = helper.getUrlEncoding({ attachments: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var through = res.headers['x-experience-api-consistent-through']; + expect(through).to.be.ok; + done(); + } + }); + }); + }); + + /** XAPI-00160, Communication 2.1.3 GET Statements + * An LRS's "X-Experience-API-Consistent-Through" header is an ISO 8601 combined date and time + */ + describe('An LRS\'s "X-Experience-API-Consistent-Through" header is an ISO 8601 combined date and time (Type, Communication 2.1.3.s2.b5).', function () { + var statement, stmtTime; + this.timeout(0); + + before('persist statement', function (done) { + var templates = [ + { statement: '{{statements.context}}' }, + { context: '{{contexts.category}}' }, + { + instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:pri@adlnet.gov" + } + } + ]; + var data = helper.createFromTemplate(templates); + statement = data.statement; + statement.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/pri'; + + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(statement) + .expect(200, done); + }); + + it('should return valid "X-Experience-API-Consistent-Through" using GET', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .wait(helper.genDelay(stmtTime, undefined, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "agent"', function (done) { + var templates = [ + { agent: '{{agents.default}}' } + ]; + var data = helper.createFromTemplate(templates); + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "verb"', function (done) { + var query = helper.getUrlEncoding({ verb: 'http://adlnet.gov/expapi/non/existent' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "activity"', function (done) { + var query = helper.getUrlEncoding({ activity: 'http://www.example.com/meetings/occurances/12345' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "registration"', function (done) { + var query = helper.getUrlEncoding({ registration: helper.generateUUID() }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "related_activities"', function (done) { + var query = helper.getUrlEncoding({ + activity: statement.context.contextActivities.category.id, + related_activities: true + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "related_agents"', function (done) { + var query = helper.getUrlEncoding({ + agent: statement.context.instructor, + related_agents: true + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "since"', function (done) { + var query = helper.getUrlEncoding({ since: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "until"', function (done) { + var query = helper.getUrlEncoding({ until: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "limit"', function (done) { + var query = helper.getUrlEncoding({ limit: 1 }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "ascending"', function (done) { + var query = helper.getUrlEncoding({ ascending: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "format"', function (done) { + var query = helper.getUrlEncoding({ format: 'ids' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + + it('should return "X-Experience-API-Consistent-Through" using GET with "attachments"', function (done) { + var query = helper.getUrlEncoding({ attachments: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var value = res.headers['x-experience-api-consistent-through']; + expect(value).to.be.ok; + var through = moment(value, moment.ISO_8601); + expect(through).to.be.ok; + expect(through.isValid()).to.be.true; + done(); + } + }); + }); + }); + + /** XAPI-00161, Communication 2.1.3 GET Statements + * An LRS's Statement API not return attachment data and only return application/json if the "attachment" parameter set to "false" + */ + describe('An LRSs Statement Resource does not return attachment data and only returns application/json if the "attachment" parameter set to "false" (Communication 2.1.3.s1.b1, XAPI-00161)', function () { + this.timeout(0); + var statementId = null; + var stmtTime = null; + + before("store statement", function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 0, + "sha2": "", + "fileUrl": "http://over.there.com/file.txt" + } + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + txtAtt1 = fs.readFileSync('test/v1_0_3/templates/attachments/simple_text1.txt'); + var t1stats = fs.statSync('test/v1_0_3/templates/attachments/simple_text1.txt'); + t1attSize = t1stats.size; + t1attHash = crypto.createHash('SHA256').update(txtAtt1).digest('hex'); + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf; + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg) + .expect(200, function (err, res) { + if (err) + done(err) + else { + var body = JSON.parse(res.body); + + statementId = body[0]; + // console.log("Statement ID is", statementId) + done(); + } + }); + }); + + it('should NOT return the attachment if "attachments" is missing', function (done) { + var query = '?statementId=' + statementId; + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, statementId)) + .headers(helper.addAllHeaders()) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + expect(res.headers["content-type"]).to.match(/^application\/json/); + done(); + } + }); + }); + + it('should NOT return the attachment if "attachments" is false', function (done) { + var query = '?statementId=' + statementId + "&attachments=false"; + + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, statementId)) + .headers(helper.addAllHeaders()) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + expect(res.headers["content-type"]).to.match(/^application\/json/); + done(); + } + }); + }); + + it('should return the attachment when "attachment" is true', function (done) { + var query = '?statementId=' + statementId + "&attachments=true"; + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, statementId)) + .headers(helper.addAllHeaders()) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + var ContentType = res.headers["content-type"]; + var type = ContentType.split(";")[0]; + expect(type).to.equal("multipart/mixed"); + var boundary = ContentType.split(";")[1].replace(" boundary=", ""); + + var body = res.body.split("--" + boundary); + var idx = -1; + for (var i in body) { + idx = Math.max(body[i].indexOf("here is a simple attachment"), idx); + } + expect(idx).to.not.eql(-1); + done(); + } + }); + + }); + + }); + + /** XAPI-00163, Communication 2.1.3 GET Statements + * An LRS's Statement API, upon processing a successful GET request, can only return a Voided Statement if that Statement is specified in the voidedStatementId parameter of that request + */ + describe('An LRS\'s Statement Resource, upon processing a successful GET request, can only return a Voided Statement if that Statement is specified in the voidedStatementId parameter of that request (Communication 2.1.4.s1.b1, XAPI-00163)', function () { + var voidedId = helper.generateUUID(); + var stmtTime; + + before('persist voided statement', function (done) { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var voided = helper.createFromTemplate(templates); + voided = voided.statement; + voided.id = voidedId; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voided) + .expect(200, done); + }); + + before('persist voiding statement', function (done) { + var templates = [ + { statement: '{{statements.voiding}}' } + ]; + var voiding = helper.createFromTemplate(templates); + voiding = voiding.statement; + voiding.object.id = voidedId; + stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voiding) + .expect(200, done); + }); + + it('should not return a voided statement if using GET "statementId"', function (done) { + this.timeout(0); + var query = helper.getUrlEncoding({ statementId: voidedId }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(404, done); + }); + }); + + /** XAPI-00162, Communication 2.1.3 GET Statements + * An LRS's Statement API processes a successful GET request using a parameter (such as stored time) which includes a voided statement and unvoided statements targeting the voided statement. The API must return 200 Ok and the statement result object, containing statements which target a voided statement, but not the voided statement itself. + */ + describe('An LRS\'s Statement Resource, upon processing a successful GET request wishing to return a Voided Statement still returns Statements which target it (Communication 2.1.4.s1.b2, XAPI-00162)', function () { + this.timeout(0); + var verbTemplate = 'http://adlnet.gov/expapi/test/voided/target/'; + var verb = verbTemplate + helper.generateUUID(); + var voidedId = helper.generateUUID(); + var voidingId = helper.generateUUID(); + var statementRefId = helper.generateUUID(); + var sinceVoidingTime, untilVoidingTime; + var stmtTime, prevStmtTime; + + before('persist voided statement', function (done) { + // console.log(new Date(Date.now() - helper.getTimeMargin()).toISOString() + ' Ed Before'); + sinceVoidingTime = new Date(Date.now() - helper.getTimeMargin() - 4000).toISOString(); + var voidedTemplates = [ + { statement: '{{statements.default}}' } + ]; + var voided = helper.createFromTemplate(voidedTemplates); + voided = voided.statement; + voided.id = voidedId; + voided.verb.id = verb; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voided) + .expect(200, done); + }); + + before('persist voiding statement', function (done) { + // console.log(new Date(Date.now() - helper.getTimeMargin()).toISOString() + ' Ing Before'); + var voidingTemplates = [ + { statement: '{{statements.voiding}}' } + ]; + var voiding = helper.createFromTemplate(voidingTemplates); + voiding = voiding.statement; + voiding.id = voidingId; + voiding.object.id = voidedId; + + prevStmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .wait(helper.genDelay(sinceVoidingTime, '?statementId=' + voidedId, voidedId)) + .headers(helper.addAllHeaders({})) + .json(voiding) + .expect(200, done); + }); + + before('persist object with statement references', function (done) { + // console.log(new Date(Date.now() - helper.getTimeMargin()).toISOString() + ' Ref Before'); + var statementRefTemplates = [ + { statement: '{{statements.object_statementref}}' } + ]; + var statementRef = helper.createFromTemplate(statementRefTemplates); + statementRef = statementRef.statement; + statementRef.id = statementRefId; + statementRef.object.id = voidedId; + statementRef.verb.id = verb; + + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .wait(helper.genDelay(prevStmtTime, '?statementId=' + voidingId, voidingId)) + .headers(helper.addAllHeaders({})) + .json(statementRef) + .expect(200, done); + }); + + before('ensure all stmts are recorded in the lrs', function (done) { + // console.log(new Date(Date.now() - helper.getTimeMargin()).toISOString() + ' Final Before'); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .wait(helper.genDelay(stmtTime, '?statementId=' + statementRefId, statementRefId)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + untilVoidingTime = new Date(Date.now() - helper.getTimeMargin() + 4000).toISOString(); + done(); + } + }); + }); + + // reworded the test to be more generic, shouldn't have to stay in here + it('should only return statements stored after designated "since" timestamp when using "since" parameter', function (done) { + // Need to use statementRefId verb b/c initial voided statement comes before voidingTime + // console.log(new Date(Date.now() - helper.getTimeMargin()).toISOString() + ' Since'); + var query = helper.getUrlEncoding({ + verb: verb, + since: sinceVoidingTime + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var results = helper.parse(res.body, done); + expect(results).to.have.property('statements'); + // console.log(results.statements.length); + var ids = []; + results.statements.forEach(function (stmt) { + ids.push(stmt.id) + }); + // console.log(ids); + expect(ids).to.contain(statementRefId); + expect(ids).to.contain(voidingId); + expect(ids).to.not.contain(voidedId); + done(); + } + }); + }); + + // reworded the test to be more generic, shouldn't have to stay in here + it('should only return statements stored at or before designated "before" timestamp when using "until" parameter', function (done) { + var query = helper.getUrlEncoding({ + verb: verb, + until: untilVoidingTime + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + try { + var results = helper.parse(res.body, done); + expect(results).to.have.property('statements'); + var ids = []; + results.statements.forEach(function (stmt) { + ids.push(stmt.id) + }); + expect(ids).to.contain(statementRefId); + expect(ids).to.contain(voidingId); + expect(ids).to.not.contain(voidedId); + done(); + } catch (e) { + if (e.message.length > 400) { + e.message = "expected results to have property 'statements' containing " + voidingId; + } + done(e); + } + } + }); + }); + + // reworded the test to be more generic, shouldn't have to stay in here + it('should return the number of statements listed in "limit" parameter', function (done) { + // console.log(new Date(Date.now() - helper.getTimeMargin()).toISOString() + ' Limit'); + var query = helper.getUrlEncoding({ + verb: verb, + limit: 1 + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var results = helper.parse(res.body, done); + expect(results).to.have.property('statements'); + expect(results.statements).to.have.length(1); + expect(results.statements[0]).to.have.property('id').to.equal(statementRefId); + done(); + } + }); + }); + + // i think this can be removed + it('should return StatementRef and voiding statement when not using "since", "until", "limit"', function (done) { + // console.log(new Date(Date.now() - helper.getTimeMargin()).toISOString() + ' None'); + var query = helper.getUrlEncoding({ + verb: verb + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var results = helper.parse(res.body, done); + expect(results).to.have.property('statements'); + expect(results.statements).to.have.length(2); + expect(results.statements[0]).to.have.property('id').to.equal(statementRefId); + expect(results.statements[1]).to.have.property('id').to.equal(voidingId); + // var pt = new Date(prevStmtTime - helper.getTimeMargin()).toISOString(); + // var st = new Date(stmtTime - helper.getTimeMargin()).toISOString(); + // console.log(sinceVoidingTime +'\n'+ pt +'\n'+ st +'\n'+ untilVoidingTime); + done(); + } + }); + }); + }); + + /** XAPI-00164, Communication 2.1.3 GET Statements + * The Statements within the "statements" property will correspond to the filtering criterion sent in with the GET request + */ + describe('The Statements within the "statements" property will correspond to the filtering criterion sent in with the GET request (Communication 2.1.3.s1, XAPI-00164)', function () { + var statement, substatement, stmtTime; + this.timeout(0); + + before('persist statement', function (done) { + var templates = [ + { statement: '{{statements.context}}' }, + { context: '{{contexts.category}}' }, + { + instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:pri@adlnet.gov" + } + } + ]; + var data = helper.createFromTemplate(templates); + statement = data.statement; + + //randomize data to prevent old results from breaking assertion logic + statement.context.contextActivities.category.id += helper.generateUUID(); + statement.verb.id += helper.generateUUID(); + statement.actor.mbox = "mailto:" + helper.generateUUID() + "@adlnet.gov"; + statement.context.registration = helper.generateUUID(); + statement.context.instructor.mbox = "mailto:" + helper.generateUUID() + "@adlnet.gov"; + statement.object.id += helper.generateUUID(); + + statement.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/pri'; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(statement) + .expect(200, done); + }); + + before('persist substatement', function (done) { + var templates = [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: '{{contexts.category}}' }, + { + instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:sub@adlnet.gov" + } + } + ]; + var data = helper.createFromTemplate(templates); + substatement = data.statement; + + //randomize data to prevent old results from breaking assertion logic + substatement.verb.id += helper.generateUUID(); + substatement.actor.mbox = "mailto:" + helper.generateUUID() + "@adlnet.gov"; + + substatement.object.verb.id += helper.generateUUID(); + substatement.object.actor.mbox = "mailto:" + helper.generateUUID() + "@adlnet.gov"; + substatement.object.object.id += helper.generateUUID(); + + + + + substatement.object.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/sub'; + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(substatement) + .expect(200, done); + }); + + it('should return StatementResult with statements as array using GET with "agent"', function (done) { + var templates = [ + { agent: statement.actor } + ]; + var data = helper.createFromTemplate(templates); + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.all.have.deep.property('actor.mbox', statement.actor.mbox); + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "verb"', function (done) { + var query = helper.getUrlEncoding({ verb: statement.verb.id }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.all.have.deep.property('verb.id', statement.verb.id); + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "activity"', function (done) { + var query = helper.getUrlEncoding({ activity: statement.object.id }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.all.have.deep.property('object.id', statement.object.id) + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "registration"', function (done) { + var query = helper.getUrlEncoding({ registration: statement.context.registration }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.all.have.deep.property('context.registration', statement.context.registration); + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "related_activities"', function (done) { + var query = helper.getUrlEncoding({ + activity: statement.context.contextActivities.category.id, + related_activities: true + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.satisfy(function (statements) { + for (var i in statements) { + if (!helper.deepSearchObject(statements[i], statement.context.contextActivities.category.id)) + return false + } + return true; + }); + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "related_agents"', function (done) { + var query = helper.getUrlEncoding({ + agent: statement.context.instructor, + related_agents: true + }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.satisfy(function (statements) { + for (var i in statements) { + if (!helper.deepSearchObject(statements[i], statement.context.instructor.mbox)) + return false; + } + return true; + }); + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "since"', function (done) { + var query = helper.getUrlEncoding({ since: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.satisfy(function (statements) { + for (var i in statements) { + if ((new Date(statements[i].stored) < new Date('2012-06-01T19:09:13.245Z'))) + return false; + } + return true; + }); + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "until"', function (done) { + var query = helper.getUrlEncoding({ until: '2012-06-01T19:09:13.245Z' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.satisfy(function (statements) { + for (var i in statements) { + if ((new Date(statements[i].stored) > new Date('2012-06-01T19:09:13.245Z'))) + return false; + } + return true; + }); + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "limit"', function (done) { + var query = helper.getUrlEncoding({ limit: 1 }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.have.length(1); + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "ascending"', function (done) { + var query = helper.getUrlEncoding({ ascending: true }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array').to.satisfy(function (statements) { + for (var i = 0; i < statements.length - 1; i++) { + var s1 = statements[i].stored; + var s2 = statements[i + 1].stored; + + if ((new Date(s1) > new Date(s2))) + return false; + } + return true; + }); + done(); + } + }); + }); + + //I think there is another test that covers the formatting requirements + it('should return StatementResult with statements as array using GET with "format"', function (done) { + var query = helper.getUrlEncoding({ format: 'ids' }); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + it('should return StatementResult with statements as array using GET with "attachments"', function (done) { + var header = { 'Content-Type': 'multipart/mixed; boundary=-------314159265358979323846' }; + var templates = [ + { statement: '{{statements.attachment}}' }, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": { "en-US": "A test attachment" }, + "description": { "en-US": "A test attachment (description)" }, + "contentType": "text/plain", + "length": 0, + "sha2": "", + "fileUrl": "http://over.there.com/file.txt" + } + ] + } + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + + txtAtt1 = fs.readFileSync('test/v1_0_3/templates/attachments/simple_text1.txt'); + var t1stats = fs.statSync('test/v1_0_3/templates/attachments/simple_text1.txt'); + t1attSize = t1stats.size; + t1attHash = crypto.createHash('SHA256').update(txtAtt1).digest('hex'); + + data.attachments[0].length = t1attSize; + data.attachments[0].sha2 = t1attHash; + + var dashes = '--'; + var crlf = '\r\n'; + var boundary = '-------314159265358979323846' + + var msg = dashes + boundary + crlf; + msg += 'Content-Type: application/json' + crlf + crlf; + msg += JSON.stringify(data) + crlf; + msg += dashes + boundary + crlf; + msg += 'Content-Type: text/plain' + crlf; + msg += 'Content-Transfer-Encoding: binary' + crlf; + msg += 'X-Experience-API-Hash: ' + data.attachments[0].sha2 + crlf + crlf; + msg += txtAtt1 + crlf; + msg += dashes + boundary + dashes + crlf; + + + var query = helper.getUrlEncoding({ attachments: true }); + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders(header)) + .body(msg) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var boundary = multipartParser.getBoundary(res.headers['content-type']); + expect(boundary).to.be.ok; + var parsed = multipartParser.parseMultipart(boundary, res.body); + expect(parsed).to.be.ok; + var results = helper.parse(parsed[0].body, done); + expect(results).to.have.property('statements'); + done(); + } + }); + } + }); + }); + }); + + /** XAPI-00???, Communication 2.1.3 GET Statements + * An LRS's Statement API rejects a GET request with additional properties other than extensions in the locations where extensions are allowed. + */ + describe('An LRS\'s Statement Resource rejects with error code 400 a GET request with additional properties than extensions in the locations where extensions are allowed', function () { + + it('should fail when using property not defined in specification', function (done) { + + let statement = helper.buildStatement(); + statement.dummy = "dummy"; + + xapiRequests.sendStatementPromise(statement) + .then(res => { + expect(res.status).to.eql(400); + done(); + }) + .catch(err => { + expect(err.response).to.not.be.undefined; + expect(err.response.status).to.eql(400); + done(); + }) + + }); + }); + + + /** XAPI-00???, Communication/timestamps 2.4.7 GET Statements + *The "timestamp" property SHOULD* be set by the LRS to the value of the "stored" property if not provided. + */ + describe('The LRS shall set the "timestamp" property to the value of the "stored" property if not provided.', function () { + + it('should set timestamp property to equal "stored" value if retrieved statement does not have its own timestamp', async function () { + let id = helper.generateUUID(); + let statement = helper.buildStatement(); + + statement.timestamp = null; + statement.id = id; + + let _ = await xapiRequests.sendStatementPromise(statement); + let res = await xapiRequests.getStatementExactPromise(id); + + let statementFromLRS = res.data; + + expect(statementFromLRS.timestamp).is.eql(statementFromLRS.stored); + }); + }); + + + /** XAPI-00???, Communication/timestamps 2.4.7 GET Statements + *An LRS SHOULD* NOT reject a timestamp for having a greater value than the current time, to prevent issues due to clock errors. + */ + describe('The LRS shall not reject a timestamp for having a greater value than the current time, within an acceptable margin of error', function () { + + it('accepts statements with greater value than current time', async function () { + + //Acceptable margin of error around five minutes + var minutes = 5; + var currentdate = new Date(); + + //add five minutes to current time + currentdate.setMinutes(currentdate.getMinutes() + minutes); + + let id = helper.generateUUID(); + let statement = helper.buildStatement(); + + statement.timestamp = currentdate.toISOString(); + statement.id = id; + + let res = await xapiRequests.sendStatementPromise(statement); + + expect(res.status).to.eql(200); + }); + }) +}); + + \ No newline at end of file diff --git a/test/v2_0/4.1.6.2-State-Resource.js b/test/v2_0/4.1.6.2-State-Resource.js new file mode 100644 index 00000000..2477959d --- /dev/null +++ b/test/v2_0/4.1.6.2-State-Resource.js @@ -0,0 +1,833 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +const request = require('super-request'); +const expect = require("chai").expect; +const helper = require("../helper"); +const xapiRequests = require("./util/requests"); + +if (global.OAUTH) + request = helper.OAuthRequest(request); + +/** Macthup with Conformance Requirements Document + * XAPI-00187 - below + * XAPI-00188 - below + * XAPI-00189 - below + * XAPI-00190 - below + * XAPI-00191 - below + * XAPI-00192 - below + * XAPI-00193 - below + * XAPI-00194 - below + * XAPI-00195 - below + * XAPI-00196 - below + * XAPI-00197 - below + * XAPI-00198 - below + * XAPI-00199 - below + * XAPI-00200 - below + * XAPI-00201 - below + * XAPI-00202 - below + * XAPI-00203 - below + * XAPI-00204 - below + * XAPI-00205 - No 'since' property with DELETE in the State Resource + * XAPI-00206 - below + * XAPI-00207 - below + * XAPI-00208 - below + * XAPI-00209 - below + * XAPI-00210 - below + * XAPI-00211 - below + * XAPI-00212 - below + * XAPI-00213 - below + * XAPI-00214 - below + * XAPI-00215 - below + * XAPI-00216 - below + * XAPI-00217 - below + * XAPI-00218 - below + * XAPI-00219 - below + * XAPI-00220 - below + * XAPI-00221 - below + * XAPI-00222 - duplicate of XAPI-00195 + * XAPI-00223 - No 'since' property with DELETE in the State Resource + * XAPI-00224 - in Parameters folder + * XAPI-00225 - in Parameters folder + * XAPI-00226 - in Parameters folder + * XAPI-00227 - below + * XAPI-00228 - in Parameters folder + * XAPI-00229 - below + * XAPI-00230 - below + * XAPI-00231 - below + * XAPI-00232 - below + * XAPI-00233 - below + * XAPI-00234 - below + * XAPI-00235 - below + */ + +describe('State Resource Requirements (Communication 2.3)', function () { + + /** XAPI-00230, Communication 2.3 State Resource + * An LRS has a State API with endpoint "base IRI"+"/activities/state" + */ + it('An LRS has a State Resource with endpoint "base IRI"+"/activities/state" (Communication 2.2.s3.table1.row1, XAPI-00230)', function () { + //Also covers An LRS will accept a POST request to the State Resource + var parameters = helper.buildState(), + document = helper.buildDocument(); + + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204); + }); + + /** XAPI-00190, Communication 2.3 State Resource + * An LRS's State API upon processing a successful PUT request returns code 204 No Content + */ + it('An LRS\'s State Resource accepts PUT requests (Communication 2.3, XAPI-00190)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('put', helper.getEndpointActivitiesState(), parameters, document, 204); + }); + + /** XAPI-00189, Communication 2.3 State Resource + * An LRS's State API upon processing a successful POST request returns code 204 No Content + */ + /** XAPI-00231, Communication 2.3 State Resource + * An LRS will accept a POST request to the State API + */ + it('An LRS\'s State Resource accepts POST requests (Communication 2.3, XAPI-00189, XAPI-00231)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204); + }); + + /** XAPI-00188, Communication 2.3 State Resource + * An LRS's State API upon processing a successful GET request returns 200 Ok, State Document + */ + it('An LRS\'s State Resource accepts GET requests (Communication 2.3, XAPI-00188)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql(document); + }); + }); + }); + + /** XAPI-00187, Communication 2.3 State Resource + * An LRS's State API upon processing a successful DELETE request returns code 204 No Content + */ + it('An LRS\'s State Resource accepts DELETE requests (Communication 2.3, XAPI-00187)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('delete', helper.getEndpointActivitiesState(), parameters, undefined, 204); + }); + }); + + /** XAPI-00192, Communication 2.3 State Resource + * An LRS's State API upon processing a successful GET request with a valid "stateId" as a parameter returns the document satisfying the requirements of the GET and code 200 OK NOTE: There is no requirement here that the LRS reacts to the "since" parameter in the case of a GET request with valid "stateId" - this is intentional + */ + it('An LRS\'s State Resource upon processing a successful GET request with a valid "stateId" as a parameter returns the document satisfying the requirements of the GET and code 200 OK (Communication 2.3.s3, XAPI-00192)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql(document); + }); + }); + }); + + /** XAPI-00191, Communication 2.3 State Resource + * An LRS's State API upon processing a successful DELETE request with a valid "stateId" as a parameter deletes the document satisfying the requirements of the DELETE and returns code 204 No Content NOTE: There is no requirement here that the LRS reacts to the "since" parameter in the case of a DELETE request with valid "stateId" - this is intentional + */ + it('An LRS\'s State Resource upon processing a successful DELETE request with a valid "stateId" as a parameter deletes the document satisfying the requirements of the DELETE and returns code 204 No Content (Communication 2.3.s3, XAPI-00191)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('delete', helper.getEndpointActivitiesState(), parameters, undefined, 204); + }); + }); + + /** XAPI-00210, Communication 2.3 State Resource + * An LRS's State API rejects a PUT request without "activityId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a PUT request without "activityId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row1, XAPI-00210)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + delete parameters.activityId; + return helper.sendRequest('put', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + + /** XAPI-00209, Communication 2.3 State Resource + * An LRS's State API rejects a POST request without "activityId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a POST request without "activityId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row1, XAPI-00209)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + delete parameters.activityId; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + + /** XAPI-00208, Communication 2.3 State Resource + * An LRS's State API rejects a GET request without "activityId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a GET request without "activityId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row1, XAPI-00208)', function () { + var parameters = helper.buildState(); + delete parameters.activityId; + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 400); + }); + + + + /** XAPI-00207, Communication 2.3 State Resource + * An LRS's State API rejects a DELETE request without "activityId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a DELETE request without "activityId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row1, XAPI-00207)', function () { + var parameters = helper.buildState(); + delete parameters.activityId; + return helper.sendRequest('delete', helper.getEndpointActivitiesState(), parameters, undefined, 400); + }); + + /** XAPI-00215, Communication 2.3 State Resource + * An LRS's State API rejects a PUT request without "agent" as a parameter with error code 400 Bad Request + */ + //+* In 1.0.3, the IRI requires a scheme, but does not in 1.0.2, thus we only test type String in this version** + it('An LRS\'s State Resource rejects a PUT request without "agent" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row2, XAPI-00215)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + delete parameters.agent; + return helper.sendRequest('put', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + + /** XAPI-00199, Communication 2.3 State Resource + * An LRS's State API rejects a PUT request with "agent" as a parameter if it is not in JSON format with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a PUT request with "agent" as a parameter if it is not in JSON format with error code 400 Bad Request (format, Communication 2.3.s3.table1.row2, XAPI-00199)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + parameters.agent = 'not JSON'; + return helper.sendRequest('put', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + + /** XAPI-00214, Communication 2.3 State Resource + * An LRS's State API rejects a POST request without "agent" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a POST request without "agent" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row2)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + delete parameters.agent; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + + /** XAPI-00198, Communication 2.3 State Resource + * An LRS's State API rejects a POST request with "agent" as a parameter if it is not in JSON format with error code 400 Bad Request + */ + describe('An LRS\'s State Resource rejects a POST request with "agent" as a parameter if it is not in JSON format with error code 400 Bad Request (format, Communication 2.3.s3.table1.row2, XAPI-00198)', function () { + + it('Should reject POST State with agent invalid value', function () { + var document = helper.buildDocument(); + var parameters = helper.buildState(); + parameters.agent = true; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + }); + + /** XAPI-00213, Communication 2.3 State Resource + * An LRS's State API rejects a GET request without "agent" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a GET request without "agent" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row2, XAPI-00213)', function () { + var parameters = helper.buildState(); + delete parameters.agent; + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 400); + }); + + /** XAPI-00197, Communication 2.3 State Resource + * An LRS's State API rejects a GET request with "agent" as a parameter if it is not in JSON format with error code 400 Bad Request + */ + describe('An LRS\'s State Resource rejects a GET request with "agent" as a parameter if it is not in JSON format with error code 400 Bad Request (format, Communication 2.3.s3.table1.row2, XAPI-00197)', function () { + + it('Should reject GET with "agent" with invalid value', function () { + var parameters = helper.buildState(); + parameters.agent = true; + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 400); + }); + }); + + /** XAPI-00212, Communication 2.3 State Resource + * An LRS's State API rejects a DELETE request without "agent" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a DELETE request without "agent" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row2, XAPI-00212)', function () { + var parameters = helper.buildState(); + delete parameters.agent; + return helper.sendRequest('delete', helper.getEndpointActivitiesState(), parameters, undefined, 400); + }); + + /** XAPI-00196, Communication 2.3 State Resource + * An LRS's State API rejects a DELETE request with "agent" as a parameter if it is not in JSON format with error code 400 Bad Request + */ + describe('An LRS\'s State Resource rejects a DELETE request with "agent" as a parameter if it is not in JSON format with error code 400 Bad Request (format, Communication 2.3.s3.table1.row2, XAPI-00196)', function () { + + it('Should reject DELETE with "agent" with invalid value', function () { + var parameters = helper.buildState(); + parameters.agent = true; + return helper.sendRequest('delete', helper.getEndpointActivitiesState(), parameters, undefined, 400); + }); + }); + + /** XAPI-00218, Communication 2.3 State Resource + * An LRS's State API can process a PUT request with "registration" as a parameter + */ + it('An LRS\'s State Resource can process a PUT request with "registration" as a parameter (multiplicity, Communication 2.3.s3.table1.row3, XAPI-00218)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + parameters.registration = helper.generateUUID(); + return helper.sendRequest('put', helper.getEndpointActivitiesState(), parameters, document, 204); + }); + + /** XAPI-00203, Communication 2.3 State Resource + * An LRS's State API rejects a PUT request with "registration" as a parameter if it is not a UUID with error code 400 Bad Request + */ + describe('An LRS\'s State Resource rejects a PUT request with "registration" as a parameter if it is not a UUID with error code 400 Bad Request(format, Communication 2.3.s3.table1.row3, XAPI-00203)', function () { + + it('Should reject PUT with "registration" with invalid value', function () { + var document = helper.buildDocument(); + var parameters = helper.buildState(); + parameters.registration = true; + return helper.sendRequest('put', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + }); + + /** XAPI-00229, Communication 2.3 State Resource + * An LRS's State API, rejects a POST request if the document is found and either document is not a valid JSON Object + */ + describe('An LRSs State Resource, rejects a POST request if the document is found and either document is not a valid JSON Object (multiplicity, Communication 2.3.s3.table1.row3, Communication 2.2.s8.b1, XAPI-00229)', function () { + // case 1 - bad post + it("If the document being posted to the State Resource does not have a Content-Type of application/json and the existing document does, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", function (done) { + var parameters = helper.buildState(); + var document = helper.buildDocument(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(document) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var document2 = 'abcdefg'; + var header2 = { 'content-type': 'not/json' }; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header2)) + .body(document2) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err) + } else { + var result = helper.parse(res.body); + expect(result).to.eql(document); + done(); + } + }); + } + }); + } + }); + }); + // case 2 - bad existing + it("If the existing document does not have a Content-Type of application/json but the document being posted to the State Resource does the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", function (done) { + var parameters = helper.buildState(); + var attachment = "/ asdf / undefined"; + var header = { 'content-type': 'application/octet-stream' }; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header)) + .body(attachment) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var attachment2 = helper.buildDocument(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(attachment2) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err) + } else { + expect(res.body).to.eql(attachment); + done(); + } + }); + } + }); + } + }); + }); + // case 3 - bad json + it("If the document being posted to the State Resource has a content type of Content-Type of application/json but cannot be parsed as a JSON Object, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", function (done) { + var parameters = helper.buildState(); + var document = helper.buildDocument() + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(document) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var header = { 'content-type': 'application/json' }; + var attachment = JSON.stringify(helper.buildState()) + '{'; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header)) + .body(attachment) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + expect(result).to.eql(document); + done(); + } + }); + } + }); + } + }); + }); + }); + + /** XAPI-00232, Communication 2.3 State Resource + * An LRS's State API, rejects a POST request if the document is found and either document's type is not "application/json" with error code 400 Bad Request + */ + it('An LRS\'s State Resource, rejects a POST request if the document is found and either document\'s type is not "application/json" with error code 400 Bad Request (Communication 2.2.s8.b1, XAPI-00232)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(), + anotherDocument = 'abc'; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, anotherDocument, 400); + }); + }); + + /** XAPI-00233, Communication 2.3 State Resource + * An LRS's State API, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document. Returning 204 No Content + */ + it('An LRS\'s State Resource, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document (Communication 2.2.s7, XAPI-00233)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql(document); + }); + }); + }); + + /** XAPI-00234, Communication 2.3 State Resource + * An LRS's State API performs a Document Merge if a profileId is found and both it and the document in the POST request have type "application/json". If the merge is successful, the LRS MUST respond with HTTP status code 204 No Content. + */ + it('An LRS\'s State Resource performs a Document Merge if a document is found and both it and the document in the POST request have type "application/json" (Communication 2.2.s7.b1, Communication 2.2.s7.b2, Communication 2.2.s7.b3, XAPI-00234)', function () { + var parameters = helper.buildState(), + document = { + car: 'Honda' + }, + anotherDocument = { + type: 'Civic' + }; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, anotherDocument, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql({ + car: 'Honda', + type: 'Civic' + }) + }); + }); + }); + }); + + /** XAPI-00235, Communication 2.3 State Resource + * An LRS must reject with 400 Bad Request a POST request to the State API which contains name/value pairs with invalid JSON and the Content-Type header is "application/json" + */ + it("An LRS must reject with 400 Bad Request a POST request to the State Resource which contains name/value pairs with invalid JSON and the Content-Type header is 'application/json' (Communication 2.3, XAPI-00235)", function (done) { + var parameters = { + activityId: 'http://www.example.com/activityId/hashset', + stateId: helper.generateUUID() + } + + var agent = encodeURIComponent(JSON.stringify({ + "objectType": "Agent", + "account": { + "homePage": "http://www.example.com/agentId/1", + "name": "Rick James" + } + }) + ).replace('%3A', '%22'); //break the encoding here. + + parameters.registration = helper.generateUUID(); + var attachment = JSON.stringify(helper.buildDocument()); + var header = { 'content-type': 'application/json' }; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesState() + '?' + helper.getUrlEncoding(parameters) + "&agent=" + agent) + .headers(helper.addAllHeaders(header)) + .body(attachment) + .expect(400, function (err, res) { + done(err); + }); + }); + + /** XAPI-00227, Communication 2.3 State Resource + * An LRS's State API can process a POST request with "registration" as a parameter + */ + it('An LRS\'s State Resource can process a POST request with "registration" as a parameter (multiplicity, Communication 2.3.s3.table1.row3, XAPI-00227)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + parameters.registration = helper.generateUUID(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204); + }); + + /** XAPI-00202, Communication 2.3 State Resource + * An LRS's State API rejects a POST request with "registration" as a parameter if it is not a UUID with error code 400 Bad Request + */ + describe('An LRS\'s State Resource rejects a POST request with "registration" as a parameter if it is not a UUID with error code 400 Bad Request (format, Communication 2.3.s3.table1.row3, XAPI-00202)', function () { + + it('Should reject POST with "registration" with invalid value', function () { + var document = helper.buildDocument(); + var parameters = helper.buildState(); + parameters.registration = true; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + }); + + /** XAPI-00220, Communication 2.3 State Resource + * An LRS's State API can process a GET request with "registration" as a parameter + */ + it('An LRS\'s State Resource can process a GET request with "registration" as a parameter (multiplicity, Communication 2.3.s3.table1.row3, XAPI-00220)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + parameters.registration = helper.generateUUID(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql(document); + }); + }); + }); + + /** XAPI-00201, Communication 2.3 State Resource + * An LRS's State API rejects a GET request with "registration" as a parameter if it is not a UUID with error code 400 Bad Request + */ + describe('An LRS\'s State Resource rejects a GET request with "registration" as a parameter if it is not a UUID with error code 400 Bad Request (format, Communication 2.3.s3.table1.row3, XAPI-00201)', function () { + + it('Should reject GET with "registration" with invalid value', function () { + var parameters = helper.buildState(); + parameters.registration = true; + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 400); + }); + }); + + /** XAPI-00219, Communication 2.3 State Resource + * An LRS's State API can process a DELETE request with "registration" as a parameter + */ + it('An LRS\'s State Resource can process a DELETE request with "registration" as a parameter (multiplicity, Communication 2.3.s3.table1.row3, XAPI-00219)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + parameters.registration = helper.generateUUID(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('delete', helper.getEndpointActivitiesState(), parameters, undefined, 204); + }); + }); + + /** XAPI-00200, Communication 2.3 State Resource + * An LRS's State API rejects a DELETE request with "registration" as a parameter if it is not a UUID with error code 400 Bad Request + */ + describe('An LRS\'s State Resource rejects a DELETE request with "registration" as a parameter if it is not a UUID with error code 400 Bad Request (format, Communication 2.3.s3.table1.row3, XAPI-00200)', function () { + + it('Should reject DELETE with "registration" with invalid value', function () { + var parameters = helper.buildState(); + parameters.registration = true; + return helper.sendRequest('delete', helper.getEndpointActivitiesState(), parameters, undefined, 400); + }); + }); + + /** XAPI-00206, Communication 2.3 State Resource + * An LRS's State API rejects a PUT request without "stateId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a PUT request without "stateId" as a parameter with error code 400 Bad Request(multiplicity, Communication 2.3.s3.table1.row4, XAPI-00206)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + delete parameters.stateId; + return helper.sendRequest('put', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + + /** XAPI-00211, Communication 2.3 State Resource + * An LRS's State API rejects a POST request without "stateId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a POST request without "stateId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row4, XAPI-00211)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + delete parameters.stateId; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 400); + }); + + /** XAPI-00217, Communication 2.3 State Resource + * An LRS's State API can process a GET request with "stateId" as a parameter + */ + it('An LRS\'s State Resource can process a GET request with "stateId" as a parameter (multiplicity, Communication 2.3.s3.table1.row4, XAPI-00217)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql(document); + }) + }); + }); + + /** XAPI-00221, Communication 2.3 State Resource + * An LRS's State API can process a GET request with "since" as a parameter. Returning 200 OK and all matching profiles after the date/time of the “since” parameter. + */ + it('An LRS\'s State Resource can process a GET request with "since" as a parameter (multiplicity, Communication 2.3.s4.table1.row4, XAPI-00221)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + var stateId = parameters.stateId; + + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + parameters.since = new Date(Date.now() - 60 * 1000 - helper.getTimeMargin()).toISOString(); // Date 1 minute ago + delete parameters.stateId; + + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.be.an('Array'); + expect(body).to.contain(stateId); + }); + }); + }); + + /** XAPI-00204, Communication 2.3 State Resource + * An LRS's State API rejects a GET request with "since" as a parameter if it is not a "TimeStamp", with error code 400 Bad Request + */ + it('An LRS\'s State Resource rejects a GET request with "since" as a parameter if it is not a "TimeStamp", with error code 400 Bad Request (format, Communication 2.3.s4.table1.row4, XAPI-00204)', function () { + var parameters = helper.buildState(); + delete parameters.stateId; + parameters.since = 'not a timestamp'; + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 400); + }); + + /** XAPI-00216, Communication 2.3 State Resource + * An LRS's State API can process a DELETE request with "stateId" as a parameter + */ + it('An LRS\'s State Resource can process a DELETE request with "stateId" as a parameter (multiplicity, Communication 2.3.s3.table1.row4, XAPI-00216)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('delete', helper.getEndpointActivitiesState(), parameters, undefined, 204); + }); + }); + + /** XAPI-00193, Communication 2.3 State Resource + * An LRS's State API upon processing a successful GET request without "stateId" as a parameter returns an array of ids of state data documents satisfying the requirements of the GET and code 200 OK + */ + //+* NOTE: **There is no requirement here that the LRS reacts to the "since" parameter in the case of a GET request with valid "stateId" - this is intentional** + it('An LRS\'s State Resource upon processing a successful GET request without "stateId" as a parameter returns an array of ids of state data documents satisfying the requirements of the GET and code 200 OK (Communication 2.3.s4, XAPI-00193)', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + delete parameters.stateId; + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.be.an('array'); + }); + }); + }); + + /** XAPI-00195, Communication 2.3 State Resource + * An LRS's returned array of ids from a successful GET request all refer to documents stored after the TimeStamp in the "since" parameter of the GET request + */ + it('An LRS\'s returned array of ids from a successful GET request to the State Resource all refer to documents stored after the TimeStamp in the "since" parameter of the GET request (Communication 2.3.s4.table1.row4, XAPI-00195)', function () { + var document = helper.buildDocument(); + var state1 = helper.buildState(); + var state2 = helper.buildState(); + var since = new Date(Date.now() - 60 * 1000 - helper.getTimeMargin()).toISOString(); //Date 1 minute ago + + return helper.sendRequest('post', helper.getEndpointActivitiesState(), state1, document, 204) + .then(function (res) { + return helper.sendRequest('post', helper.getEndpointActivitiesState(), state2, document, 204) + .then(function (res) { + var parameters = helper.buildState(); + delete parameters.stateId; + parameters.since = since; + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.be.an('array'); + expect(body).to.have.length.above(1); + expect(body).to.contain(state1.stateId); + expect(body).to.contain(state2.stateId); + }); + }); + }); + }); + + /** XAPI-00194, Communication 2.3 State Resource + * An LRS's State API upon processing a successful DELETE request without "stateId" as a parameter deletes documents satisfying the requirements of the DELETE and code 204 No Content + */ + //+* NOTE: **There is no requirement here that the LRS reacts to the "since" parameter in the case of a GET request with valid "stateId" - this is intentional** + it('An LRS\'s State Resource upon processing a successful DELETE request without "stateId" as a parameter deletes documents satisfying the requirements of the DELETE and code 204 No Content (Communication 2.3.s5, XAPI-00194)', function () { + var parameters = helper.buildState(); + parameters.activityId = parameters.activityId + helper.generateUUID(); + + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, helper.buildDocument(), 204) + .then(function () { + delete parameters.stateId; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), helper.buildState(), helper.buildDocument(), 204) + .then(function () { + return helper.sendRequest('delete', helper.getEndpointActivitiesState(), parameters, undefined, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.be.an('array'); + expect(body).to.have.length(0); + }); + }); + }); + }); + }); + + describe("The LRS shall include a Last-Modified header indicating when the document was last modified.", function() { + + let document = helper.buildDocument(); + let updatedDocument = { + ...document, + name: "Updated Name:" + helper.generateUUID() + }; + let resourcePath = xapiRequests.resourcePaths.activityState; + let resourceParams = helper.buildState(); + + before ("Add the document", async() => { + + await xapiRequests.deleteDocument(resourcePath, resourceParams); + await xapiRequests.postDocument(resourcePath, document, resourceParams); + }); + + it("Returns a Last-Modified header at all", async() => { + + let res = await xapiRequests.getDocuments(resourcePath, resourceParams); + + let modifiedStr = res.headers.get("last-modified"); + let modifiedDate = Date.parse(modifiedStr); + + expect(modifiedDate).to.not.be.NaN; + }); + + it("Updates the Last-Modified value when the corresponding document is updated.", async() => { + + let originalDocRes = await xapiRequests.getDocuments(resourcePath, resourceParams); + await xapiRequests.delay(1500); + + let updateRes = await xapiRequests.postDocument(resourcePath, updatedDocument, resourceParams); + expect(updateRes.status).to.eql(204); + + let updatedDocRes = await xapiRequests.getDocuments(resourcePath, resourceParams); + + let headerBeforeUpdate = Date.parse(originalDocRes.headers.get("last-modified")); + let headerAfterUpdate = Date.parse(updatedDocRes.headers.get("last-modified")); + + expect(headerAfterUpdate).to.be.greaterThan(headerBeforeUpdate); + }); + + /** + * As-written, this is not currently a requirement for xAPI 2.0. + * + * It is present in the changelog, but not in the Multiple-GET documentation for an LRS. + */ + // it("Provides the Last-Modified value matching the most recently updated document.", async() => { + + // let agent = { + // "objectType": "Agent", + // "account": { + // "homePage": "http://www.example.com/state/multiple-last-modified", + // "name": "State: Multiple Last Modified" + // } + // }; + + // let stateA = {...helper.buildState(), agent}; + // let stateB = {...helper.buildState(), agent}; + + // await xapiRequests.postDocument(resourcePath, document, stateA); + // await xapiRequests.postDocument(resourcePath, updatedDocument, stateB); + + // let resA = await xapiRequests.getDocuments(resourcePath, stateA); + // let resB = await xapiRequests.getDocuments(resourcePath, stateB); + + // let modifiedA = Date.parse(resA.headers.get("last-modified")); + // let modifiedB = Date.parse(resB.headers.get("last-modified")); + + // let earliestTime = modifiedA > modifiedB ? modifiedA : modifiedB; + // let latestTime = modifiedA > modifiedB ? modifiedA : modifiedB; + + // let groupParams = { + // ...stateA, + // since: new Date(earliestTime).toUTCString() + // }; + // delete groupParams.stateId; + + // let groupRes = await xapiRequests.getDocuments(resourcePath, groupParams); + // let groupTime = Date.parse(groupRes.headers.get("last-modified")); + + // expect(groupTime).to.equal(latestTime); + // }); + }); +}); diff --git a/test/v2_0/4.1.6.3-Agents-Resource.js b/test/v2_0/4.1.6.3-Agents-Resource.js new file mode 100644 index 00000000..55a5a603 --- /dev/null +++ b/test/v2_0/4.1.6.3-Agents-Resource.js @@ -0,0 +1,265 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Agents Resource Requirements (Communication 2.4)', function () { + +/** Matchup with Conformance Requirements Document + * XAPI-00236 - below + * XAPI-00237 - below + * XAPI-00238 - below + * XAPI-00239 - below + * XAPI-00240 - below + * XAPI-00241 - below + * XAPI-00242 - below + * XAPI-00243 - below + * XAPI-00244 - below + * XAPI-00245 - below + * XAPI-00246 - same as 248 - The Agents Resource MUST have an endpoint which accepts GET requests and returns a special, Person Object where each attribute has an array value and it is legal to include multiple identifying properties. + * XAPI-00247 - same as 248 - If an LRS does not have any additional information about an Agent to return from the Agents Resource, the LRS MUST still return a Person when queried, but that Person Object will only include the information associated with the requested Agent. + * XAPI-00248 - below + * XAPI-00249 - below + */ + +/** XAPI-00245, Communication 2.4 Agents Resource + * An LRS has an Agents API with endpoint "base IRI" + /agents" + */ + it('An LRS has an Agents Resource with endpoint "base IRI" + /agents" (Communication 2.4, XAPI-00245) **Implicit** (in that it is not named this by the spec)', function () { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + var parameters = { + agent: data.statement.actor + } + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgents(), parameters, undefined, 200); + }); + }); + +/** XAPI-00236, Communication 2.4 Agents Resource + * An LRS's Agents API accepts GET requests with response 200 OK, Person Object + */ + it('An LRS\'s Agents Resource accepts GET requests (Communication 2.4.s2, XAPI-00236)', function () { + return helper.sendRequest('get', helper.getEndpointAgents(), helper.buildAgent(), undefined, 200); + }); + +/** XAPI-00248, Communication 2.4 Agents Resource + * An LRS's Agents API upon processing a successful GET request returns a Person Object based on matched data from the "agent" parameter and code 200 OK + */ + it('An LRS\'s Agent Resource upon processing a successful GET request returns a Person Object if the "agent" parameter can be found in the LRS and code 200 OK (Communication 2.4.s2.table1.row1, XAPI-00248)', function () { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + var parameters = { + agent: statement.actor + } + return helper.sendRequest('get', helper.getEndpointAgents(), parameters, undefined, 200) + .then(function (res) { + expect(res.body.objectType).to.eql("Person"); + expect(res.body).to.be.an('object'); + }); + }); + }); + +/** XAPI-00243, Communication 2.4 Agents Resource + * An LRS's Agents API rejects a GET request without "agent" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Agents Resource rejects a GET request without "agent" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.4.s2.table1.row1, XAPI-00243)', function () { + return helper.sendRequest('get', helper.getEndpointAgents(), undefined, undefined, 400); + }); + +/** XAPI-00237, Communication 2.4 Agents Resource + * A Person Object's "objectType" property is a String and is "Person" The LRS must return a valid “objectType” string. + */ + it('A Person Object\'s "objectType" property is a String and is "Person" (Format, Vocabulary, Communication 2.4.s5.table1.row1, XAPI-00237)', function () { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgents(), { agent: statement.actor }, undefined, 200) + .then(function (res) { + var person = res.body; + expect(person).to.have.property('objectType').to.equal('Person'); + }); + }); + }); + +/** XAPI-00238, Agents Resource + * A Person Object's "name" property is an Array of Strings. The LRS must return a “name” property with a valid Array of Strings, if present. + */ + it('A Person Object\'s "name" property is an Array of Strings (Multiplicity, Communication 2.4.s5.table1.row2, XAPI-00238)', function () { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgents(), { agent: statement.actor }, undefined, 200) + .then(function (res) { + var person = res.body; + expect(person).to.have.property('name').to.be.an('array'); + person.name.forEach(function(item){ + expect(item).to.be.a('string'); + }); + }); + }); + }); + +/** XAPI-00239, Communication 2.4 Agents Resource + * A Person Object's "mbox" property is an Array of IRIs. The LRS must return an “mbox” property with a valid array of IRIs, if present. + */ + it('A Person Object\'s "mbox" property is an Array of IRIs (Multiplicity, Communication 2.4.s5.table1.row3, XAPI-00239)', function () { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + var MAIL_TO = 'mailto:'; + var isEmail = require('isemail'); + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgents(), { agent: statement.actor }, undefined, 200) + .then(function (res) { + var person = res.body; + expect(person).to.have.property('mbox').to.be.an('array'); + person.mbox.forEach(function(item){ + expect(item).to.be.a('string'); + var email = item.substring(MAIL_TO.length); + expect(isEmail(email)).to.be.true; + }); + }); + }); + }); + +/** XAPI-00244, Communication 2.4 Agents Resource + * A Person Object's "mbox" entries have the form "mailto:emailaddress". The LRS must return a Person Object which has a “mbox” value with the form "mailto:emailaddress" + */ + it('A Person Object\'s "mbox" entries have the form "mailto:emailaddress" (Format, Communication 2.4.s5.table1.row3, XAPI-00244)', function () { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgents(), { agent: statement.actor }, undefined, 200) + .then(function (res) { + var person = res.body; + expect(person).to.have.property('mbox').to.be.an('array'); + person.mbox.forEach(function(item){ + expect(item).to.be.a('string'); + expect(item).to.match(/^mailto:/); + }); + }); + }); + }); + +/** XAPI-00240, Communication 2.4 Agents Resource + * A Person Object's "mbox_sha1sum" property is an Array of Strings. The LRS must return a Person Object which has a “mbox_sha1sum” and is valid array of strings, if present. + */ + it('A Person Object\'s "mbox_sha1sum" property is an Array of Strings (Multiplicity, Communication 2.4.s5.table1.row4, XAPI-00240)', function () { + var templates = [ + {statement: '{{statements.no_actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgents(), { agent: statement.actor }, undefined, 200) + .then(function (res) { + var person = res.body; + expect(person).to.have.property('mbox_sha1sum').to.be.an('array'); + person.mbox_sha1sum.forEach(function(item){ + expect(item).to.be.a('string'); + }); + }); + }); + }); + +/** XAPI-00241, Communication 2.4 Agents Resource + * A Person Object's "openid" property is an Array of Strings The LRS must return a “openid” value which is valid array of strings, if present. + */ + it('A Person Object\'s "openid" property is an Array of Strings (Multiplicity, Communication 2.4.s5.table1.row5, XAPI-00241)', function () { + var templates = [ + {statement: '{{statements.no_actor}}'}, + {actor: '{{agents.openid}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgents(), { agent: statement.actor }, undefined, 200) + .then(function (res) { + var person = res.body; + expect(person).to.have.property('openid').to.be.an('array'); + person.openid.forEach(function(item){ + expect(item).to.be.a('string'); + }); + }); + }); + }); + +/** XAPI-00242, Communication 2.4 Agents Resource + * A Person Object's "account" property is an Array of Account Objects The LRS must return a Person Object with a “name” value which is a valid array of account objects, if present. + */ + it('A Person Object\'s "account" property is an Array of Account Objects (Multiplicity, Communication 2.4.s5.table1.row6, XAPI-00242)', function () { + var templates = [ + {statement: '{{statements.no_actor}}'}, + {actor: '{{agents.account}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgents(), { agent: statement.actor }, undefined, 200) + .then(function (res) { + var person = res.body; + expect(person).to.have.property('account').to.be.an('array'); + person.account.forEach(function(item){ + expect(item).to.be.an('object'); + }); + }); + }); + }); + +/** XAPI-00249, Communication 2.4 Agents Resource + * An LRSs Agents API rejects a GET request with "agent" as a parameter if it is not a valid (in structure) Agent with error code 400 Bad Request (XAPI-00249) + */ + it('An LRSs Agents Resource rejects a GET request with "agent" as a parameter if it is not a valid, in structure, Agent with error code 400 Bad Request (Communication 2.4, XAPI-00249)',function() + { + var parameter = helper.buildAgent(); + delete parameter.agent.account; + return helper.sendRequest('get', helper.getEndpointAgents(), parameter, undefined, 400); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('../helper'), require('../multipartParser'), require('../redirect.js'))); diff --git a/test/v2_0/4.1.6.4-Activity-Resource.js b/test/v2_0/4.1.6.4-Activity-Resource.js new file mode 100644 index 00000000..04a15474 --- /dev/null +++ b/test/v2_0/4.1.6.4-Activity-Resource.js @@ -0,0 +1,148 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +const request = require('super-request'); +const expect = require("chai").expect; +const helper = require("../helper"); +const xapiRequests = require("./util/requests"); + +if (global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Activities Resource Requirements (Communication 2.5)', function() { + + /** + * XAPI-00250 - below + * XAPI-00251 - below + * XAPI-00252 - below + * XAPI-00253 - below + * XAPI-00254 - below + */ + + /** XAPI-00252, Communication 2.5 Activities Resource + * An LRS has an Activities API with endpoint "base IRI" + /activities" (7.5) Implicit (in that it is not named this by the spec). + */ + it('An LRS has an Activities Resource with endpoint "base IRI" + /activities" (Communication 2.5, Implicit) **Implicit** (in that it is not named this by the spec)', function () { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + var parameters = { + activityId: data.statement.object.id + } + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivities(), parameters, undefined, 200); + }); + }); + + /** XAPI-00253, Communication 2.5 Activities Resource + * An LRS's Activities API accepts GET requests. + */ + it('An LRS\'s Activities Resource accepts GET requests (Communication 2.5, XAPI-00253)', function () { + var templates = [ + { statement: '{{statements.default}}' } + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + var parameters = { + activityId: data.statement.object.id + } + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivities(), parameters, undefined, 200); + }); + }); + + /** XAPI-00251, Communication 2.5 Activities Resource + * An LRS's Activities API upon processing a successful GET request returns 200 OK and the complete Activity Object. + */ + it('An LRS\'s Activities Resource upon processing a successful GET request returns the complete Activity Object (Communication 2.5.s1)', function () { + var templates = [ + { statement: '{{statements.object_activity}}' }, + { object: '{{activities.default}}' } + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + statement.object.id = 'http://www.example.com/verify/complete/34534'; + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + var parameters = { + activityId: statement.object.id + }; + return helper.sendRequest('get', helper.getEndpointActivities(), parameters, undefined, 200) + .then(function (res) { + var activity = res.body; + expect(activity).to.be.ok; + expect(activity).to.eql(statement.object); + }); + }); + }); + + /** XAPI-00250, Communication 2.5 Activities Resource + * An LRS's Activities API rejects a GET request without "activityId" as a parameter with error code 400 Bad Request. + */ + it('An LRS\'s Activities Resource rejects a GET request without "activityId" as a parameter with error code 400 Bad Request (multiplicity, Communication.md#2.5.s1.table1.row1, XAPI-00250)', function () { + return helper.sendRequest('get', helper.getEndpointActivities(), undefined, undefined, 400); + }); + + // Note: Tests focusing on type "String" as a parameter are likely to be stricken or reworded before final release. + // Also note: Using an it over an it nullifies the inner its, consider using a describe. + it('An LRS\'s Activities Resource rejects a GET request with "activityId" as a parameter if it is not type "String" with error code 400 Bad Request (format, Communication 2.5.s1.table1.row1)', function () { + + it('Should reject GET with "activityId" with invalid value', function () { + var parameters = helper.buildActivity(); + parameters.activityId = true; + return helper.sendRequest('get', helper.getEndpointActivities(), parameters, undefined, 400); + }); + }); + + /** XAPI-00254, Communication 2.5 Activities Resource + * The Activity Object must contain all available information about an activity from any statements who target the same “activityId”. + * For example, LRS accepts two statements each with a different language description of an activity using the exact same “activityId”. + * The LRS must return both language descriptions when a GET request is made to the Activities endpoint for that “activityId”. + */ + it('The Activity Object must contain all available information about an activity from any statements who target the same "activityId". For example, LRS accepts two statements each with a different language description of an activity using the exact same "activityId". The LRS must return both language descriptions when a GET request is made to the Activities endpoint for that "activityId" (multiplicity, Communication.md#2.5.s1.table1.row1, XAPI-00254)', function () { + + var templates = [ + { statement: '{{statements.object_activity}}' }, + { object: '{{activities.default}}' } + ]; + var data = helper.createFromTemplate(templates); + var data2 = helper.createFromTemplate(templates); + var statement = data.statement; + var statement2 = data2.statement; + statement.object.id = 'http://www.example.com/verify/complete/34534100123'; + statement2.object.id = 'http://www.example.com/verify/complete/34534100123'; + + statement2.object.definition.name['fr-FR'] = "réunion"; + delete statement2.object.definition.name['en-US']; + + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement, statement2], 200) + .then(function () { + var parameters = { + activityId: statement.object.id + }; + return helper.sendRequest('get', helper.getEndpointActivities(), parameters, undefined, 200) + .then(function (res) { + var activity = res.body; + expect(activity.definition.name['en-US']).to.eql("example meeting"); + expect(activity.definition.name['fr-FR']).to.eql("réunion"); + }); + }); + }); + + it("If an LRS does not have a canonical definition of the Activity to return, the LRS shall still return an Activity Object when queried.", async() => { + + let randomIRI = 'http://www.example.com/never-before-seen-activityId/' + helper.generateUUID(); + let res = await xapiRequests.getActivityWithIRI(randomIRI); + + expect(res.status).to.eql(200); + expect(res.data.id).to.eql(randomIRI); + expect(res.data.objectType).to.eql("Activity"); + }); +}); diff --git a/test/v2_0/4.1.6.5-Agent-Profile-Resource.js b/test/v2_0/4.1.6.5-Agent-Profile-Resource.js new file mode 100644 index 00000000..34d386c6 --- /dev/null +++ b/test/v2_0/4.1.6.5-Agent-Profile-Resource.js @@ -0,0 +1,648 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +const request = require('super-request'); +const expect = require("chai").expect; +const helper = require("../helper"); +const xapiRequests = require("./util/requests"); + +if (global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Agent Profile Resource Requirements (Communication 2.6)', function() { + + /** Matchup with Conformance + * XAPI-00255 - below + * XAPI-00256 - below + * XAPI-00257 - below + * XAPI-00258 - below + * XAPI-00259 - below + * XAPI-00260 - below + * XAPI-00261 - below + * XAPI-00262 - below + * XAPI-00263 - below + * XAPI-00264 - below + * XAPI-00265 - below + * XAPI-00266 - below + * XAPI-00267 - below + * XAPI-00268 - below + * XAPI-00269 - below + * XAPI-00270 - below + * XAPI-00271 - below + * XAPI-00272 - below + * XAPI-00273 - below + * XAPI-00274 - below + * XAPI-00275 - below + * XAPI-00276 - in parameters folder + * XAPI-00277 - in parameters folder + * XAPI-00278 - below + * XAPI-00279 - below + * XAPI-00280 - below + * XAPI-00281 - below + * XAPI-00282 - below + * XAPI-00283 - below + * XAPI-00284 - below + */ + + /** XAPI-00282, Communication 2.6 Agent Profile Resource + * An LRS has an Agent Profile API with endpoint "base IRI"+"/agents/profile" + */ + describe('An LRS has an Agent Profile Resource with endpoint "base IRI"+"/agents/profile" (Communication 2.2.s3.table2.row3.a, Communication 2.2.table2.row3.c, XAPI-00282)', function () { + + /** XAPI-00274, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API accepts valid GET requests with code 200 OK, Profile document + */ + it('An LRS\'s Agent Profile Resource accepts GET requests (Communication 2.6.s2, XAPI-00274)', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 200); + }); + }); + + /** XAPI-00273, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API upon processing a successful PUT request returns code 204 No Content + */ + it('An LRS\'s Agent Profile Resource upon processing a successful PUT request returns code 204 No Content (Communication 2.6.s3, XAPI-00273)', function (done) { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointAgentsProfile() + "?" + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({ "If-None-Match": "*" })) + .json(document) + .expect(204, done); + }); + + /** XAPI-00272, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API upon processing a successful POST request returns code 204 No Content + */ + /** XAPI-00283, Communication 2.6 Agent Profile Resource + * An LRS will accept a POST request to the Agent Profile API + */ + it('An LRS\'s Agent Profile Resource upon processing a successful POST request returns code 204 No Content (Communication 2.6.s3, XAPI-00272, XAPI-00283)', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204); + }); + + /** XAPI-00271, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API upon processing a successful DELETE request deletes the associated profile and returns code 204 No Content + */ + it('An LRS\'s Agent Profile Resource upon processing a successful DELETE request deletes the associated profile and returns code 204 No Content (Communication 2.6.s3, XAPI-00271)', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('delete', helper.getEndpointAgentsProfile(), parameters, undefined, 204) + }); + }); + }); // describe + + /** XAPI-00259, Communication 2.6 Agent Profile Resource + * The Agent Profile API MUST return 200 OK - Profile Content when a GET request is received with a valid agent JSON Object. + */ + /** XAPI-00269, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API upon processing a successful GET request with a valid Agent Object and valid "profileId" as a parameter returns the document satisfying the requirements of the GET and code 200 OK + */ + it('An LRS\'s Agent Profile Resource upon processing a successful GET request with a valid "profileId" as a parameter returns the document satisfying the requirements of the GET and code 200 OK (Communication 2.6.s3, XAPI-00259, XAPI-00269)', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql(document); + }) + }); + }); + + /** XAPI-00264, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a PUT request without "agent" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Agent Profile Resource rejects a PUT request without "agent" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.6.s3.table1.row1, XAPI-00264)', function (done) { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + delete parameters.agent; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({ "If-None-Match": "*" })) + .json(document) + .expect(400, done); + }); + + /** XAPI-00257, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a PUT request with "agent" as a parameter if it is not an Agent Object with error code 400 Bad Request + */ + describe('An LRS\'s Agent Profile Resource rejects a PUT request with "agent" as a parameter if it is not an Agent Object with error code 400 Bad Request (format, Communication 2.6.s3.table1.row1, XAPI-00257)', function () { + + it('Should reject PUT with "agent" with invalid value', function (done) { + var document = helper.buildDocument(); + var parameters = helper.buildAgentProfile(); + parameters.agent = true; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({ "If-None-Match": "*" })) + .json(document) + .expect(400, done); + }); + }); + + /** XAPI-00263, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a POST request without "agent" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Agent Profile Resource rejects a POST request without "agent" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.6.s3.table1.row1, XAPI-00263)', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + delete parameters.agent; + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 400); + }); + + /** XAPI-00256, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a POST request with "agent" as a parameter if it is not an Agent Object with error code 400 Bad Request + */ + it('An LRS\'s Agent Profile Resource rejects a POST request with "agent" as a parameter if it is not an Agent Object with error code 400 Bad Request (format, Communication 2.6.s3.table1.row1, XAPI-00256)', function () { + + it('Should reject POST with "agent" with invalid value', function () { + var document = helper.buildDocument(); + var parameters = helper.buildAgentProfile(); + parameters.agent = true; + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 400); + }); + }); + + /** XAPI-00262, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a DELETE request without "agent" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Agent Profile Resource rejects a DELETE request without "agent" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.6.s3.table1.row1, XAPI-00262)', function () { + var parameters = helper.buildAgentProfile(); + delete parameters.agent; + return helper.sendRequest('delete', helper.getEndpointAgentsProfile(), parameters, undefined, 400); + }); + + /** XAPI-00255, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a DELETE request with "agent" as a parameter if it is not an Agent Object with error code 400 Bad Request + */ + describe('An LRS\'s Agent Profile Resource rejects a DELETE request with "agent" as a parameter if it is not an Agent Object with error code 400 Bad Request (format, Communication 2.6.s3.table1.row1, XAPI-00255)', function () { + + it('Should reject DELETE with "agent" with invalid value', function () { + var document = helper.buildDocument(); + var parameters = helper.buildAgentProfile(); + parameters.agent = true; + return helper.sendRequest('delete', helper.getEndpointAgentsProfile(), parameters, document, 400); + }); + }); + + /** XAPI-00258, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a GET request with "agent" as a parameter if it is not an Agent Object with error code 400 Bad Request + */ + describe('An LRS\'s Agent Profile Resource rejects a GET request with "agent" as a parameter if it is a valid, in structure, Agent with error code 400 Bad Request (multiplicity, Communication 2.6.s4.table1.row1, Communication 2.6.s3.table1.row1, XAPI-00258)', function () { + it('Should reject GET with "agent" with invalid value', function () { + var parameters = helper.buildAgentProfile(); + parameters.agent = true; + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 400); + }); + }); + + /** XAPI-00267, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a PUT request without "profileId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Agent Profile Resource rejects a PUT request without "profileId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.6.s3.table1.row2, XAPI-00267)', function (done) { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + delete parameters.profileId; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({ "If-None-Match": "*" })) + .json(document) + .expect(400, done); + }); + + /** XAPI-00266, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a POST request without "profileId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Agent Profile Resource rejects a POST request without "profileId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.6.s3.table1.row2, XAPI-00266)', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + delete parameters.profileId; + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 400); + }); + + /** XAPI-00265, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a DELETE request without "profileId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Agent Profile Resource rejects a DELETE request without "profileId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.6.s3.table1.row2, XAPI-00265)', function () { + var parameters = helper.buildAgentProfile(); + delete parameters.profileId; + return helper.sendRequest('delete', helper.getEndpointAgentsProfile(), parameters, undefined, 400); + }); + + /** XAPI-00270, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API upon processing a successful GET request with a valid Agent Object and without "profileId" as a parameter returns an array of ids of agent profile documents satisfying the requirements of the GET and code 200 OK + */ + it('An LRS\'s Agent Profile Resource upon processing a successful GET request without "profileId" as a parameter returns an array of ids of agent profile documents satisfying the requirements of the GET and code 200 OK (Communication 2.6.s4, XAPI-00270)', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function () { + delete parameters.profileId; + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.have.length.above(0); + }); + }); + }); + + /** XAPI-00261, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a GET request without "agent" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Agent Profile Resource rejects a GET request without "agent" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.6.s4.table1.row1, XAPI-00261)', function () { + var parameters = helper.buildAgentProfile(); + delete parameters.agent; + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 400); + }); + + /** XAPI-00268, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API can process a GET request with "since" as a parameter. Returning 200 OK and all matching profiles after the date/time of the “since” parameter + */ + it('An LRS\'s Agent Profile Resource can process a GET request with "since" as a parameter (Multiplicity, Communication 2.6.s4.table1.row2, XAPI-00268)', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function () { + parameters.since = new Date(Date.now() - 60 * 1000 - helper.getTimeMargin()).toISOString(); //Date one minute ago + delete parameters.profileId + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 200); + }); + }); + + /** XAPI-00260, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API rejects a GET request with "since" as a parameter if it is not a "TimeStamp", with error code 400 Bad Request + */ + describe('An LRS\'s Agent Profile Resource rejects a GET request with "since" as a parameter if it is not a "TimeStamp", with error code 400 Bad Request (format, Communication 2.6.s4.table1.row2, XAPI-00260)', function () { + it('Should reject GET with "since" with invalid value', function () { + var parameters = helper.buildAgentProfile(); + delete parameters.profileId + parameters.since = true; + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 400); + }); + }); + + /** XAPI-00275, Communication 2.6 Agent Profile Resource + * The Agent Profile API's returned array of ids from a successful GET request all refer to documents stored after the TimeStamp in the "since" parameter of the GET request if such a parameter was present + */ + it('An LRS\'s returned array of ids from a successful GET request to the Agent Profile Resource all refer to documents stored after the TimeStamp in the "since" parameter of the GET request if such a parameter was present (Communication 2.6.s4.table1.row2, XAPI-00275)', function () { + var parameters = helper.buildAgentProfile(), + profile1 = parameters.profileId; + document = helper.buildDocument(); + var since = new Date(Date.now() - 60 * 1000 - helper.getTimeMargin()).toISOString(); //Date one minute ago + + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function () { + parameters.since = since; + delete parameters.profileId; + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.be.an('array'); + expect(body).to.have.length.above(0); + expect(body).to.contain(profile1); + }) + }); + }); + + /** XAPI-00279, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API performs a Document Merge if a profileId is found and both it and the document in the POST request have type "application/json" If the merge is successful, the LRS MUST respond with HTTP status code 204 No Content. + * not quite, but is this close enough?? + */ + it('An LRS\'s Agent Profile Resource performs a Document Merge if a document is found and both it and the document in the POST request have type "application/json" (Communication 2.2.s7.b1, Communication 2.2.s7.b2, Communication 2.2.s7.b3, XAPI-00279)', function () { + var parameters = helper.buildAgentProfile(), + document = { + car: 'Honda' + }, + anotherDocument = { + type: 'Civic' + }; + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, anotherDocument, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql({ + car: 'Honda', + type: 'Civic' + }) + }); + }); + }); + }); + + /** XAPI-00280, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document.Returning 204 No Content + */ + it('An LRS\'s Agent Profile Resource, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document (Communication 2.2.s7, XAPI-00280)', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointAgentsProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql(document); + }) + }); + }); + + /** XAPI-00278, Communication 2.6 Agent Profile Resource + * An LRS's Agent Profile API, rejects a POST request if the document is found and either document's type is not "application/json" with error code 400 Bad Request + */ + describe('An LRSs Agent Profile Resource, rejects a POST request if the document is found and either documents type is not "application/json" with error code 400 Bad Request (multiplicity, Communication 2.3.s3.table1.row3, Communication 2.2.s8.b1, XAPI-00278)', function () { + // case 1 - bad post + it("If the document being posted to the Agent Profile Resource does not have a Content-Type of application/json and the existing document does, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", function (done) { + var parameters = helper.buildAgentProfile(); + var document = helper.buildDocument(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(document) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var document2 = "abcdefg"; + var header2 = { 'content-type': 'application/octet-stream' }; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header2)) + .body(document2) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + expect(result).to.eql(document); + done(err); + } + }); + } + }); + } + }); + }); + // case 2 - bad existing + it("If the existing document does not have a Content-Type of application/json but the document being posted to the Agent Profile Resource does the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", function (done) { + var parameters = helper.buildAgentProfile(); + var attachment = "/ asdf / undefined"; + var header = { 'content-type': 'application/octet-stream', 'If-None-Match': '*' }; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header)) + .body(attachment) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var attachment2 = helper.buildDocument(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(attachment2) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err); + } else { + expect(res.body).to.eql(attachment); + done(); + } + }); + } + }); + } + }); + }); + // case 3 - bad json + it("If the document being posted to the Agent Profile Resource has a content type of Content-Type of application/json but cannot be parsed as a JSON Object, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", function (done) { + var parameters = helper.buildAgentProfile(); + var document = helper.buildDocument() + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(document) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var header = { 'content-type': 'application/json' }; + var attachment = JSON.stringify(helper.buildAgentProfile()) + '{'; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header)) + .body(attachment) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + expect(result).to.eql(document); + done(); + } + }); + } + }); + } + }); + }); + }); + + /** XAPI-00281, Communication 2.6 Agent Profile Resource + * An LRS must reject with 400 Bad Request a POST request to the Activitiy Profile API which contains name/value pairs with invalid JSON and the Content-Type header is "application/json" + */ + it("An LRS's Agent Profile Resource, rejects a POST request if the document is found and either document is not a valid JSON Object (Communication 2.6, XAPI-00281)", function (done) { + var parameters = helper.buildAgentProfile(); + var document = helper.buildDocument(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(document) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var document2 = 'abcdefg'; + var header2 = { 'content-type': 'not/json' }; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header2)) + .body(document2) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err) + } else { + var result = helper.parse(res.body); + expect(result).to.eql(document); + done(); + } + }); + } + }); + } + }); + }); + + /** XAPI-00284, Communication 2.6 Agent Profile Resource + * An LRS must reject with 400 Bad Request a POST request to the Activitiy Profile API which contains name/value pairs with invalid JSON and the Content-Type header is "application/json" + */ + it("An LRS must reject with 400 Bad Request a POST request to the Activitiy Profile Resource which contains name/value pairs with invalid JSON and the Content-Type header is 'application/json' (Communication 2.6, XAPI-00284)", function (done) { + var parameters = { + profileId: helper.generateUUID() + } + + var agent = encodeURIComponent(JSON.stringify({ + "objectType": "Agent", + "account": { + "homePage": "http://www.example.com/agentId/1", + "name": "Rick James" + } + }) + ).replace('%3A', '%22'); //break the encoding here. + + var attachment = JSON.stringify(helper.buildDocument()); + var header = { 'content-type': 'application/json' }; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointAgentsProfile() + '?' + helper.getUrlEncoding(parameters) + "&agent=" + agent) + .headers(helper.addAllHeaders(header)) + .body(attachment) + .expect(400, done); + }); + + describe("The LRS shall include a Last-Modified header indicating when the document was last modified.", function() { + + let document = helper.buildDocument(); + let updatedDocument = { + ...document, + name: "Updated Name:" + helper.generateUUID() + }; + let resourcePath = xapiRequests.resourcePaths.agentsProfile; + let resourceParams = helper.buildAgentProfile(); + + before ("Add the document", async() => { + + await xapiRequests.deleteDocument(resourcePath, resourceParams); + await xapiRequests.postDocument(resourcePath, document, resourceParams); + }); + + it("Returns a Last-Modified header at all", async() => { + + let res = await xapiRequests.getDocuments(resourcePath, resourceParams); + + let modifiedStr = res.headers.get("last-modified"); + let modifiedDate = Date.parse(modifiedStr); + + expect(modifiedDate).to.not.be.NaN; + }); + + it("Updates the Last-Modified value when the corresponding document is updated.", async() => { + + let originalDocRes = await xapiRequests.getDocuments(resourcePath, resourceParams); + await xapiRequests.delay(1500); + + let updateRes = await xapiRequests.postDocument(resourcePath, updatedDocument, resourceParams); + expect(updateRes.status).to.eql(204); + + let updatedDocRes = await xapiRequests.getDocuments(resourcePath, resourceParams); + + let headerBeforeUpdate = Date.parse(originalDocRes.headers.get("last-modified")); + let headerAfterUpdate = Date.parse(updatedDocRes.headers.get("last-modified")); + + expect(headerAfterUpdate).to.be.greaterThan(headerBeforeUpdate); + }); + + /** + * As-written, this is not currently a requirement for xAPI 2.0. + * + * It is present in the changelog, but not in the Multiple-GET documentation for an LRS. + */ + // it("Provides the Last-Modified value matching the most recently updated document.", async() => { + + // let agent = { + // "objectType": "Agent", + // "account": { + // "homePage": "http://www.example.com/agent-profile/multiple-last-modified", + // "name": "Agent Profile: Multiple Last Modified" + // } + // }; + + // let profileA = {...helper.buildAgentProfile(), agent}; + // let profileB = {...helper.buildAgentProfile(), agent}; + + // await xapiRequests.postDocument(resourcePath, document, profileA); + // await xapiRequests.postDocument(resourcePath, updatedDocument, profileB); + + // let resA = await xapiRequests.getDocuments(resourcePath, profileA); + // let resB = await xapiRequests.getDocuments(resourcePath, profileB); + + // let modifiedA = Date.parse(resA.headers.get("last-modified")); + // let modifiedB = Date.parse(resB.headers.get("last-modified")); + + // let earliestTime = modifiedA > modifiedB ? modifiedA : modifiedB; + // let latestTime = modifiedA > modifiedB ? modifiedA : modifiedB; + + // let groupParams = { + // agent: profileA.agent, + // since: new Date(earliestTime).toUTCString() + // }; + + // let groupRes = await xapiRequests.getDocuments(resourcePath, groupParams); + // let groupTime = Date.parse(groupRes.headers.get("last-modified")); + + // expect(groupTime).to.equal(latestTime); + // }); + }); +}); diff --git a/test/v2_0/4.1.6.6-Activity-Profile-Resource.js b/test/v2_0/4.1.6.6-Activity-Profile-Resource.js new file mode 100644 index 00000000..3f1f826a --- /dev/null +++ b/test/v2_0/4.1.6.6-Activity-Profile-Resource.js @@ -0,0 +1,563 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ +const request = require('super-request'); +const expect = require("chai").expect; +const helper = require("../helper"); +const xapiRequests = require("./util/requests"); + +if (global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Activity Profile Resource Requirements (Communication 2.7)', function() { + + /** Matchup with + * XAPI-00285 - below + * XAPI-00286 - below + * XAPI-00287 - below + * XAPI-00288 - below + * XAPI-00289 - below + * XAPI-00290 - below + * XAPI-00291 - below + * XAPI-00292 - below + * XAPI-00293 - below + * XAPI-00294 - below + * XAPI-00295 - below + * XAPI-00296 - below + * XAPI-00297 - below + * XAPI-00298 - below + * XAPI-00299 - below + * XAPI-00300 - below + * XAPI-00301 - below + * XAPI-00302 - below + * XAPI-00303 - below + * XAPI-00304 - 'agent' is not a valid parameter in the Activity Profile Resource + * XAPI-00305 - in Parameters folder + * XAPI-00306 - in Parameters folder + * XAPI-00307 - in Parameters folder + * XAPI-00308 - below + * XAPI-00309 - below + * XAPI-00310 - below + * XAPI-00311 - below + * XAPI-00312 - below + * XAPI-00313 - below + * XAPI-00314 - below + */ + + /** XAPI-00311, Communication 2.7 Activity Profile Resource + * An LRS has an Activity Profile API with endpoint "base IRI"+"/activities/profile" + */ + it('An LRS has an Activity Profile Resource with endpoint "base IRI"+"/activities/profile" (Communication 2.2.s3.table1.row2, XAPI-00311)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204); + }); + + /** XAPI-00287, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API upon processing a successful PUT request returns code 204 No Content + */ + /** XAPI-00293, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API accepts PUT requests + */ + describe('An LRS\'s Activity Profile Resource accepts PUT requests (Communication 2.7, XAPI-00287, XAPI-00293)', function () { + it('passes with 204 no content', function (done) { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointActivitiesProfile() + "?" + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({ "If-None-Match": "*" })) + .json(document) + .expect(204, done); + }); + }); // describe + + /** XAPI-00286, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API upon processing a successful POST request returns code 204 No Content + */ + /** XAPI-00292, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API accepts POST requests + */ + /** XAPI-00312, Communication 2.7 Activity Profile Resource + * An LRS will accept a POST request to the Activity Profile API + */ + it('An LRS\'s Activity Profile Resource accepts POST requests (Communication 2.7, XAPI-00286, XAPI-00292, XAPI-00312)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204); + }); + + /** XAPI-00285, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API upon processing a successful DELETE request deletes the associated profile and returns code 204 No Content + */ + /** XAPI-00291, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API accepts DELETE requests + */ + it('An LRS\'s Activity Profile Resource accepts DELETE requests (Communication 2.7, XAPI-00285, XAPI-00291)', function () { + var parameters = helper.buildActivityProfile(); + return helper.sendRequest('delete', helper.getEndpointActivitiesProfile(), parameters, undefined, 204); + }); + + /** XAPI-00290, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API accepts GET requests + */ + it('An LRS\'s Activity Profile Resource accepts GET requests (Communication 2.7, XAPI-00290)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesProfile(), parameters, undefined, 200); + }); + }); + + + /** XAPI-00288, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API upon processing a successful GET request with a valid "profileId" as a parameter returns the document satisfying the requirements of the GET and code 200 OK + */ + it('An LRS\'s Activity Profile Resource upon processing a successful GET request with a valid "profileId" as a parameter returns the document satisfying the requirements of the GET and code 200 OK (Communication 2.7.s3, XAPI-00288)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql(document); + }) + }); + }); + + /** XAPI-00299, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API rejects a PUT request without "activityId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Activity Profile Resource rejects a PUT request without "activityId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.7.s3.table1.row1, XAPI-00299)', function (done) { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + delete parameters.activityId; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointActivitiesProfile() + "?" + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({ 'If-None-Match': '*' })) + .json(document) + .expect(400, done); + }); + + /** XAPI-00298, Communication 2.7 Activity Profile Resources + * An LRS's Activity Profile API rejects a POST request without "activityId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Activity Profile Resource rejects a POST request without "activityId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.7.s3.table1.row1, XAPI-00298)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + delete parameters.activityId; + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 400); + }); + + /** XAPI-00297, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API rejects a DELETE request without "activityId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Activity Profile Resource rejects a DELETE request without "activityId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.7.s3.table1.row1, XAPI-00297)', function () { + var parameters = helper.buildActivityProfile(); + delete parameters.activityId; + return helper.sendRequest('delete', helper.getEndpointActivitiesProfile(), parameters, undefined, 400); + }); + + /** XAPI-00296, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API rejects a GET request without "activityId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Activity Profile Resource rejects a GET request without "activityId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.7.s3.table1.row1, Communication 2.7.s4.table1.row1, XAPI-00296)', function () { + var parameters = helper.buildActivityProfile(); + delete parameters.activityId; + return helper.sendRequest('get', helper.getEndpointActivitiesProfile(), parameters, undefined, 400); + }); + + /** XAPI-00302, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API rejects a PUT request without "profileId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Activity Profile Resource rejects a PUT request without "profileId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.7.s3.table1.row2, XAPI-00302)', function (done) { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + delete parameters.profileId; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointActivitiesProfile() + "?" + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({ 'If-None-Match': '*' })) + .json(document) + .expect(400, done); + }); + + /** XAPI-00301, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API rejects a POST request without "profileId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Activity Profile Resource rejects a POST request without "profileId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.7.s3.table1.row2, XAPI-00301)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + delete parameters.profileId; + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 400); + }); + + /** XAPI-00300, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API rejects a DELETE request without "profileId" as a parameter with error code 400 Bad Request + */ + it('An LRS\'s Activity Profile Resource rejects a DELETE request without "profileId" as a parameter with error code 400 Bad Request (multiplicity, Communication 2.7.s3.table1.row2, XAPI-00300)', function () { + var parameters = helper.buildActivityProfile(); + delete parameters.profileId; + return helper.sendRequest('delete', helper.getEndpointActivitiesProfile(), parameters, undefined, 400); + }); + + /** XAPI-00289, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API upon processing a successful GET request without "profileId" as a parameter returns an array of ids of activity profile documents satisfying the requirements of the GET and code 200 OK + */ + it('An LRS\'s Activity Profile Resource upon processing a successful GET request without "profileId" as a parameter returns an array of ids of activity profile documents satisfying the requirements of the GET and code 200 OK (Communication 2.7.s4, XAPI-00289)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + parameters.activityId = parameters.activityId + helper.generateUUID(); + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + delete parameters.profileId; + return helper.sendRequest('get', helper.getEndpointActivitiesProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.be.an('array'); + expect(body).to.be.length.above(0); + }); + }); + }); + + /** XAPI-00303, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API can process a GET request with "since" as a parameter. Returning 200 OK and all matching profiles after the date/time of the “since” parameter. + */ + it('An LRS\'s Activity Profile Resource can process a GET request with "since" as a parameter (multiplicity, Communication 2.7.s4.table1.row2, XAPI-00303)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + delete parameters.profileId; + parameters.since = new Date(Date.now() - 60 * 1000 - helper.getTimeMargin()).toISOString(); //Date one minute ago + + return helper.sendRequest('get', helper.getEndpointActivitiesProfile(), parameters, undefined, 200); + }); + }); + + /** XAPI-00295, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API rejects a GET request with "since" as a parameter if it is not a "TimeStamp", with error code 400 Bad Request + */ + describe('An LRS\'s Activity Profile Resource rejects a GET request with "since" as a parameter if it is not a "TimeStamp", with error code 400 Bad Request (format, Communication 2.7.s4.table1.row2, XAPI-00295)', function () { + it('Should reject GET with "since" with invalid value', function () { + var parameters = helper.buildActivityProfile(); + parameters.since = true; + delete parameters.profileId; + + return helper.sendRequest('get', helper.getEndpointActivitiesProfile(), parameters, undefined, 400); + }); + }); + + /** XAPI-00294, Communication 2.7 Activity Profile Resource + * The Activity Profile API's returned array of ids from a successful GET request all refer to documents stored after the TimeStamp in the "since" parameter of the GET request if such a parameter was present + */ + it('An LRS\'s returned array of ids from a successful GET request to the Activity Profile Resource all refer to documents stored after the TimeStamp in the "since" parameter of the GET request if such a parameter was present (Communication 2.7.s4.table1.row2, XAPI-00294)', function () { + var parameters = helper.buildActivityProfile(), + profile1 = parameters.profileId; + document = helper.buildDocument(); + parameters.activityId = parameters.activityId + helper.generateUUID(); + var since = new Date(Date.now() - 60 * 1000 - helper.getTimeMargin()).toISOString(); + + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + delete parameters.profileId; + parameters.since = since; + return helper.sendRequest('get', helper.getEndpointActivitiesProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.be.an('array'); + expect(body).to.be.length.above(0); + expect(body).to.contain(profile1); + }); + }); + }); + + /** XAPI-00310, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document. Returning 204 No Content + */ + it('An LRS\'s Activity Profile Resource, upon receiving a POST request for a document not currently in the LRS, treats it as a PUT request and store a new document (Communication 2.2.s7, XAPI-00310)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql(document); + }) + }); + }); + + /** XAPI-00308, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API performs a Document Merge if a activityId is found and both it and the document in the POST request have type "application/json" If the merge is successful, the LRS MUST respond with HTTP status code 204 No Content. + * activityId?? + */ + it('An LRS\'s Activity Profile Resource performs a Document Merge if a document is found and both it and the document in the POST request have type "application/json" (Communication 2.2.s7.b1, Communication 2.2.s7.b2, Communication 2.2.s7.b3, XAPI-00308)', function () { + var parameters = helper.buildActivityProfile(), + document = { + car: 'Honda' + }, + anotherDocument = { + type: 'Civic' + }; + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, anotherDocument, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesProfile(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql({ + car: 'Honda', + type: 'Civic' + }) + }); + }); + }); + }); + + /** XAPI-00309, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API, rejects a POST request if the document is found and either document's type is not "application/json" with error code 400 Bad Request + */ + it('An LRS\'s Activity Profile Resource, rejects a POST request if the document is found and either document\'s type is not "application/json" with error code 400 Bad Request (Communication 2.2.s8.b1, XAPI-00309)', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(), + anotherDocument = 'abc'; + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, anotherDocument, 400); + }); + }); + + /** XAPI-00313, Communication 2.7 Activity Profile Resource + * An LRS's Activity Profile API, rejects a POST request if the document is found and either doucment is not a valid JSON Object + */ + describe('An LRS\'s Activity Profile Resource, rejects a POST request if the document is found and either document is not a valid JSON Object (Communication 2.7.s3.table1.row3, Communication 2.2.s8.b1, XAPI-00313)', function () { + // case 1 - bad post + it('If the document being posted to the Activity Profile Resource does not have a Content-Type of application/json and the existing document does, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.', function (done) { + var document = helper.buildActivityProfile(); + var parameters = helper.buildActivityProfile(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(document) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var document2 = 'abcdefg'; + var header2 = { 'content-type': 'application/octet-stream' }; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header2)) + .body(document2) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + expect(result).to.eql(document); + done(); + } + }); + } + }); + } + }); + }); + // case 2 - bad existion + it("If the existing document does not have a Content-Type of application/json but the document being posted to the Activity Profile Resource does the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", function (done) { + var parameters = helper.buildActivityProfile(); + var attachment = "/ asdf / undefined"; + var header = { 'content-type': 'application/octet-stream', 'If-None-Match': '*' }; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header)) + .body(attachment) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var attachment2 = helper.buildDocument(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(attachment2) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err) + } else { + expect(res.body).to.eql(attachment); + done(); + } + }); + } + }); + } + }); + }); + // case 3 - bad json + it("If the document being posted to the Activity Profile Resource has a content type of Content-Type of application/json but cannot be parsed as a JSON Object, the LRS MUST respond with HTTP status code 400 Bad Request, and MUST NOT update the target document as a result of the request.", function (done) { + var parameters = helper.buildActivityProfile(); + var document = helper.buildDocument() + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .json(document) + .expect(204, function (err, res) { + if (err) { + done(err); + } else { + var header = { 'content-type': 'application/json' }; + var attachment = JSON.stringify(helper.buildActivityProfile()) + '{'; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders(header)) + .body(attachment) + .expect(400, function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({})) + .expect(200, function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + expect(result).to.eql(document); + done(); + } + }); + } + }); + } + }); + }); + }); + + /** XAPI-00314, Communication 2.7 Activity Profile Resource + * An LRS's must reject, with 400 Bad Request, a POST request to the Activity Profile API which contains name/value pairs with invalid JSON and the Content-Type header is "application/json" + */ + it('An LRS\'s must reject, with 400 Bad Request, a POST request to the Activity Profile Resource which contains name/value pairs with invalid JSON and the Content-Type header is "application/json" (Communication 2.7.s4.table1.row2, XAPI-00314)', function (done) { + + var document = JSON.stringify(helper.buildDocument()) + '['; + var parameters = helper.buildActivityProfile(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointActivitiesProfile() + '?' + helper.getUrlEncoding(parameters)) + .headers(helper.addAllHeaders({ 'Content-Type': 'application/json' })) + .body(document) + .expect(400, done); + }); + + describe("The LRS shall include a Last-Modified header indicating when the document was last modified.", function () { + + let document = helper.buildDocument(); + let updatedDocument = { + ...document, + name: "Updated Name:" + helper.generateUUID() + }; + let resourcePath = xapiRequests.resourcePaths.activityProfile; + let resourceParams = helper.buildActivityProfile(); + + before("Add the document", async () => { + + await xapiRequests.deleteDocument(resourcePath, resourceParams); + await xapiRequests.postDocument(resourcePath, document, resourceParams); + }); + + it("Returns a Last-Modified header at all", async () => { + + let res = await xapiRequests.getDocuments(resourcePath, resourceParams); + + let modifiedStr = res.headers.get("last-modified"); + let modifiedDate = Date.parse(modifiedStr); + + expect(modifiedDate).to.not.be.NaN; + }); + + it("Updates the Last-Modified value when the corresponding document is updated.", async () => { + + let originalDocRes = await xapiRequests.getDocuments(resourcePath, resourceParams); + await xapiRequests.delay(1500); + + let updateRes = await xapiRequests.postDocument(resourcePath, updatedDocument, resourceParams); + expect(updateRes.status).to.eql(204); + + let updatedDocRes = await xapiRequests.getDocuments(resourcePath, resourceParams); + + let headerBeforeUpdate = Date.parse(originalDocRes.headers.get("last-modified")); + let headerAfterUpdate = Date.parse(updatedDocRes.headers.get("last-modified")); + + expect(headerAfterUpdate).to.be.greaterThan(headerBeforeUpdate); + }); + + /** + * As-written, this is not currently a requirement for xAPI 2.0. + * + * It is present in the changelog, but not in the Multiple-GET documentation for an LRS. + */ + // it("Provides the Last-Modified value matching the most recently updated document.", async() => { + + // let agent = { + // "objectType": "Agent", + // "account": { + // "homePage": "http://www.example.com/activity-profile/multiple-last-modified", + // "name": "Activity Profile: Multiple Last Modified" + // } + // }; + + // let profileA = {...helper.buildActivityProfile(), agent}; + // let profileB = {...helper.buildActivityProfile(), agent}; + + // await xapiRequests.postDocument(resourcePath, document, profileA); + // await xapiRequests.postDocument(resourcePath, updatedDocument, profileB); + + // let resA = await xapiRequests.getDocuments(resourcePath, profileA); + // let resB = await xapiRequests.getDocuments(resourcePath, profileB); + + // let modifiedA = Date.parse(resA.headers.get("last-modified")); + // let modifiedB = Date.parse(resB.headers.get("last-modified")); + + // let earliestTime = modifiedA > modifiedB ? modifiedA : modifiedB; + // let latestTime = modifiedA > modifiedB ? modifiedA : modifiedB; + + // let groupParams = { + // activityId: profileA.activityId, + // since: new Date(earliestTime).toUTCString() + // }; + + // let groupRes = await xapiRequests.getDocuments(resourcePath, groupParams); + // let groupTime = Date.parse(groupRes.headers.get("last-modified")); + + // expect(groupTime).to.equal(latestTime); + // }); + }); +}); \ No newline at end of file diff --git a/test/v2_0/4.1.6.7-About-Resource.js b/test/v2_0/4.1.6.7-About-Resource.js new file mode 100644 index 00000000..a5ddea50 --- /dev/null +++ b/test/v2_0/4.1.6.7-About-Resource.js @@ -0,0 +1,306 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('About Resource Requirements (Communication 2.8)', function () { + +/** Matchup with Conformance Requirements Document + * XAPI-00315 - below + * XAPI-00316 - below + * XAPI-00317 - below + * XAPI-00318 - below + * XAPI-00319 - below + * XAPI-00320 - bad test - extesion property is optional in spec + * XAPI-00321 - below + */ + +/** XAPI-00315, Communication 2.8 About Resource + * An LRS has an About API with endpoint "base IRI"+"/about" + */ + it('An LRS has an About Resource with endpoint "base IRI"+"/about" (Communication 2.8, XAPI-00315)', function () { + return helper.sendRequest('get', '/about', undefined, undefined, 200); + }); + +/** XAPI-00319, Communication 2.8 About Resource + * An LRS's About Resource accepts GET requests. Upon processing a successful GET request returns a version property and code 200 OK + */ + it('An LRS\'s About Resource upon processing a successful GET request returns a version property and code 200 OK (multiplicity, Communication 2.8.s4, XAPI-00319)', function () { + return helper.sendRequest('get', '/about', undefined, undefined, 200) + .then(function (res) { + var about = res.body; + expect(about).to.have.property('version'); + }); + }); + +/** XAPI-00318, Communication 2.8 About Resource + * An LRS's About API's version property is an array of strings + */ + it('An LRS\'s About Resource\'s version property is an array of strings (format, Communication 2.8.s4.table1.row1, XAPI-00318)', function () { + return helper.sendRequest('get', '/about', undefined, undefined, 200) + .then(function (res) { + var about = res.body; + expect(about).to.have.property('version').to.be.an('array'); + }); + }); + +/** XAPI-00317, Communication 2.8 About Resource + * An LRS's About API's version property contains at least one string of "1.0.x" + */ + it('An LRS\'s About Resource\'s version property contains at least one string of "2.0.0" (Communication 2.8.s5.b1.b1, XAPI-00317)', function () { + return helper.sendRequest('get', '/about', undefined, undefined, 200) + .then(function (res) { + var about = res.body; + expect(about).to.have.property('version').to.be.an('array'); + + var foundVersion = false + about.version.forEach(function (item) { + if (item === '2.0.0') { + foundVersion = true; + } + }) + expect(foundVersion).to.be.true; + }); + }); + +/** XAPI-00316, Communication 2.8 About Resource + * An LRS's About API's version property can only have values of "0.9", "0.95", "1.0.0", or “1.0.x” with + */ + it('An LRS\'s About Resource\'s version property can only have values of "0.9", "0.95", "1.0.0", or ""1.0." + X" with (Communication 2.8.s5.b1.b1, XAPI-00316)', function () { + return helper.sendRequest('get', '/about', undefined, undefined, 200) + .then(function (res) { + var about = res.body; + expect(about).to.have.property('version').to.be.an('array'); + // var validVersions = ['0.9', '0.95', '1.0.0', '1.0.1', '1.0.2', '1.0.3', '2.0.0']; + // about.version.forEach(function (item) { + // expect(validVersions).to.include(item); + // }); + + expect(about.version).to.include("2.0.0"); + }); + }); + +/** XAPI-00321, Communication 2.8 About Resource + * An LRS rejects with error code 400 Bad Request, a Request which does not use a "X-Experience-API-Version" header name to any API except the About API + */ + describe('An LRS rejects with error code 400 Bad Request, a Request which does not use a "X-Experience-API-Version" header name to any Resource except the About Resource (multiplicity, Communication 2.8.s4.table1.row2, XAPI-00321)', function () { + + it ('using Statement Endpoint', function(done){ + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .headers(helper.addBasicAuthenicationHeader({})) + .end(function (err, res) { + if (err) { + done(err); + } else if (res.statusCode === 400) { + // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected + expect(res.statusCode).to.eql(400); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); + done(); + } else if (res.statusCode === 200) { + // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers + expect(res.statusCode).to.eql(200); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^0?\.9\d*?$/); + done(); + } else { + // at this point there was some error and we pass along the message + var str = 'Received: status code - ' + res.statusCode + ' from LRS of version '; + if (res.headers['x-experience-api-version']) { + str += res.headers['x-experience-api-version']; + } else { + str += 'missing'; + } + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; + done(new Error(str)); + } + }); + }); + + /* + it ('using About Endpoint', function(done){ + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointAbout()) + .headers(helper.addBasicAuthenicationHeader({})) + .expect(200, done) + }); + */ + + it ('using Activities Endpoint', function(done){ + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointActivities()) + .headers(helper.addBasicAuthenicationHeader({})) + .end(function (err, res) { + if (err) { + done(err); + } else if (res.statusCode === 400) { + // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected + expect(res.statusCode).to.eql(400); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); + done(); + } else if (res.statusCode === 200) { + // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers + expect(res.statusCode).to.eql(200); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^0?\.9\d*?$/); + done(); + } else { + // at this point there was some error and we pass along the message + var str = 'Received: status code - ' + res.statusCode + ' from LRS of version '; + if (res.headers['x-experience-api-version']) { + str += res.headers['x-experience-api-version']; + } else { + str += 'missing'; + } + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; + done(new Error(str)); + } + }); + }); + + it ('using Activities Profile Endpoint', function(done){ + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointActivitiesProfile()) + .headers(helper.addBasicAuthenicationHeader({})) + .end(function (err, res) { + if (err) { + done(err); + } else if (res.statusCode === 400) { + // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected + expect(res.statusCode).to.eql(400); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); + done(); + } else if (res.statusCode === 200) { + // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers + expect(res.statusCode).to.eql(200); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^0?\.9\d*?$/); + done(); + } else { + // at this point there was some error and we pass along the message + var str = 'Received: status code - ' + res.statusCode + ' from LRS of version '; + if (res.headers['x-experience-api-version']) { + str += res.headers['x-experience-api-version']; + } else { + str += 'missing'; + } + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; + done(new Error(str)); + } + }); + }); + + it ('using Activities State Endpoint', function(done){ + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointActivitiesState()) + .headers(helper.addBasicAuthenicationHeader({})) + .end(function (err, res) { + if (err) { + done(err); + } else if (res.statusCode === 400) { + // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected + expect(res.statusCode).to.eql(400); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); + done(); + } else if (res.statusCode === 200) { + // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers + expect(res.statusCode).to.eql(200); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^0?\.9\d*?$/); + done(); + } else { + // at this point there was some error and we pass along the message + var str = 'Received: status code - ' + res.statusCode + ' from LRS of version '; + if (res.headers['x-experience-api-version']) { + str += res.headers['x-experience-api-version']; + } else { + str += 'missing'; + } + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; + done(new Error(str)); + } + }); + }); + + it ('using Agents Endpoint', function(done){ + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointAgents()) + .headers(helper.addBasicAuthenicationHeader({})) + .end(function (err, res) { + if (err) { + done(err); + } else if (res.statusCode === 400) { + // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected + expect(res.statusCode).to.eql(400); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); + done(); + } else if (res.statusCode === 200) { + // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers + expect(res.statusCode).to.eql(200); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^0?\.9\d*?$/); + done(); + } else { + // at this point there was some error and we pass along the message + var str = 'Received: status code - ' + res.statusCode + ' from LRS of version '; + if (res.headers['x-experience-api-version']) { + str += res.headers['x-experience-api-version']; + } else { + str += 'missing'; + } + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; + done(new Error(str)); + } + }); + }); + + it ('using Agents Profile Endpoint', function(done){ + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointAgentsProfile()) + .headers(helper.addBasicAuthenicationHeader({})) + .end(function (err, res) { + if (err) { + done(err); + } else if (res.statusCode === 400) { + // if the status code is a 400, we expect that the request was handled and rejected by a 1.0.x compliant lrs and test for that version in the result headers or that the the request was forwarded to a 0.9x lrs and rejected + expect(res.statusCode).to.eql(400); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.\d+$|^1\.0\.\d+$|^0?\.9\d*?$/); + done(); + } else if (res.statusCode === 200) { + // if the status code is a 200, we expect that the request was rerouted to a 0.9x compliant lrs and test for that version in the result headers + expect(res.statusCode).to.eql(200); + expect(res.headers['x-experience-api-version']).to.exist; + expect(res.headers['x-experience-api-version']).to.match(/^0?\.9\d*?$/); + done(); + } else { + // at this point there was some error and we pass along the message + var str = 'Received: status code - ' + res.statusCode + ' from LRS of version '; + if (res.headers['x-experience-api-version']) { + str += res.headers['x-experience-api-version']; + } else { + str += 'missing'; + } + str += '.\nExpected: either 400 with LRS version 1.0.x, 2.0.x, or 200 with LRS version 0.9x.'; + done(new Error(str)); + } + }); + }); + + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/4.2.2.1-Actor-Requirements.js b/test/v2_0/4.2.2.1-Actor-Requirements.js new file mode 100644 index 00000000..1693e92b --- /dev/null +++ b/test/v2_0/4.2.2.1-Actor-Requirements.js @@ -0,0 +1,52 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +/** Matchup with Conformance Requirements Document + * XAPI-00031 - in actors.js + + * 2.4.2.1 Actor is Agent - may have more in agents.js + * XAPI-00032 - in agents.js + * XAPI-00033 - in agents.js + * XAPI-00034 - in agents.js + + * 2.4.2.2 Actor is Group + * XAPI-00035 - in groups.js + * XAPI-00036 - in groups.js + * XAPI-00037 - in groups.js - multiple suites + + * 2.4.2.3 Inverse Function Identifier + * XAPI-00038 - in ifis.js - two suites + * XAPI-00039 - in ifis.js + * XAPI-00040 - in ifis.js + * XAPI-00041 - in ifis.js + + * 2.4.2.4 Account Object + * XAPI-00042 - in accountobjects.js + * XAPI-00043 - in accountobjects.js + */ + +describe('Actor Property Requirements (Data 2.4.2)', () => { + + //Data 2.4.2 + templatingSelection.createTemplate("actors.js"); + //Data 2.4.2.1 + templatingSelection.createTemplate("agents.js"); + //Data 2.4.2.2 + templatingSelection.createTemplate("groups.js"); + //Data 2.4.2.3 + templatingSelection.createTemplate("ifis.js"); + //Data 2.4.2.4 + templatingSelection.createTemplate("accountobjects.js"); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.2.2-Verb-Requirements.js b/test/v2_0/4.2.2.2-Verb-Requirements.js new file mode 100644 index 00000000..161ea415 --- /dev/null +++ b/test/v2_0/4.2.2.2-Verb-Requirements.js @@ -0,0 +1,24 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +/** Matchup with Conformance Requirements Document + * XAPI-00044 - in verbs.js - two suites + * XAPI-00045 - in verbs.js + */ + +describe('Verb Property Requirements (Data 2.4.3)', () => { + + templatingSelection.createTemplate('verbs.js'); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.2.3-Object-Requirements.js b/test/v2_0/4.2.2.3-Object-Requirements.js new file mode 100644 index 00000000..96aec79b --- /dev/null +++ b/test/v2_0/4.2.2.3-Object-Requirements.js @@ -0,0 +1,317 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Object Property Requirements (Data 2.4.4)', () => { + + //Data 2.4.4 object +/** Matchup with Conformance Requirements Document + * XAPI-00046 - in objects.js + */ + templatingSelection.createTemplate('objects.js'); + + //Data 2.4.4.1 when objectType is activity +/** Matchup with Conformance Requirements Document + * XAPI-00047 - in activities.js + * XAPI-00048 - in activities.js + * XAPI-00049 - in activities.js + * XAPI-00050 - in activities.js + * XAPI-00051 - in activities.js + * XAPI-00052 - in activities.js + * XAPI-00053 - in activities.js + * XAPI-00054 - in activities.js + * XAPI-00055 - in activities.js + * XAPI-00056 - in activities.js + * XAPI-00057 - in activities.js + * XAPI-00058 - in activities.js + * XAPI-00059 - in activities.js + * XAPI-00060 - in activities.js + * XAPI-00061 - in activities.js + * XAPI-00062 - in activities.js + * XAPI-00063 - in activities.js + * XAPI-00064 - below + */ + templatingSelection.createTemplate("activities.js"); + + /** XAPI-00064, Data 2.4.4.1 when objectType is activity + * An Activity Definition uses the "interactionType" property if correctResponsesPattern is present. An LRS rejects a statement with 400 Bad Request if a correctResponsePattern is present and interactionType is not. + */ + describe('An Activity Definition uses the "interactionType" property if any of the correctResponsesPattern, choices, scale, source, target, or steps properties are used (Multiplicity, Data 2.4.4.1.s8, XAPI-00064) **Implicit**', function () { + + it ('Activity Definition uses correctResponsesPattern without "interactionType" property',function(done){ + id = helper.generateUUID(); + var correctResponsesPatterntemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.other}}'} + ]; + correctResponsesPattern = helper.createFromTemplate(correctResponsesPatterntemplates); + correctResponsesPattern = correctResponsesPattern.statement; + delete correctResponsesPattern.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(correctResponsesPattern).expect(400, done); + }); + + it ('Activity Definition uses choices without "interactionType" property',function(done){ + id = helper.generateUUID(); + var choicetemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.choice}}'} + ]; + choice = helper.createFromTemplate(choicetemplates); + choice = choice.statement; + delete choice.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(choice).expect(400, done); + }); + + it ('Activity Definition uses fill-in without "interactionType" property',function(done){ + id = helper.generateUUID(); + var fillintemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.fill_in}}'} + ]; + fillin = helper.createFromTemplate(fillintemplates); + fillin = fillin.statement; + delete fillin.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(fillin).expect(400, done); + }); + + it ('Activity Definition uses scale without "interactionType" property',function(done){ + id = helper.generateUUID(); + var scaletemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.likert}}'} + ]; + scale = helper.createFromTemplate(scaletemplates); + scale = scale.statement; + delete scale.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(scale).expect(400, done); + }); + + it ('Activity Definition uses long-fill-in without "interactionType" property',function(done){ + id = helper.generateUUID(); + var fillintemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.long_fill_in}}'} + ]; + fillin = helper.createFromTemplate(fillintemplates); + fillin = fillin.statement; + delete fillin.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(fillin).expect(400, done); + }); + + it ('Activity Definition uses source without "interactionType" property',function(done){ + id = helper.generateUUID(); + var sourcetemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.matching}}'} + ]; + source = helper.createFromTemplate(sourcetemplates); + source = source.statement; + delete source.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(source).expect(400, done); + }); + + it ('Activity Definition uses target without "interactionType" property',function(done){ + id = helper.generateUUID(); + var targettemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.matching_target}}'} + ]; + target = helper.createFromTemplate(targettemplates); + target = target.statement; + delete target.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(target).expect(400, done); + }); + + it ('Activity Definition uses numeric without "interactionType" property',function(done){ + id = helper.generateUUID(); + var numerictemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.numeric}}'} + ]; + numeric = helper.createFromTemplate(numerictemplates); + numeric = numeric.statement; + delete numeric.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(numeric).expect(400, done); + }); + + it ('Activity Definition uses other without "interactionType" property',function(done){ + id = helper.generateUUID(); + var othertemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.other}}'} + ]; + other = helper.createFromTemplate(othertemplates); + other = other.statement; + delete other.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(other).expect(400, done); + }); + + it ('Activity Definition uses performance without "interactionType" property',function(done){ + id = helper.generateUUID(); + var stepstemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.performance}}'} + ]; + steps = helper.createFromTemplate(stepstemplates); + steps = steps.statement; + delete steps.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(steps).expect(400, done); + }); + + it ('Activity Definition uses sequencing without "interactionType" property',function(done){ + id = helper.generateUUID(); + var seqtemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.sequencing}}'} + ]; + seq = helper.createFromTemplate(seqtemplates); + seq = seq.statement; + delete seq.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(seq).expect(400, done); + }); + + it ('Activity Definition uses true-false without "interactionType" property',function(done){ + id = helper.generateUUID(); + var tftemplates = [ + {statement: '{{statements.default}}'}, + {object: '{{activities.true_false}}'} + ]; + tf = helper.createFromTemplate(tftemplates); + tf = tf.statement; + delete tf.object.definition.interactionType; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(tf).expect(400, done); + }); + + + }); + + //Data 2.4.4.2 - when the object is an agent or a group +/** Matchup with Conformance Requirements Document + * XAPI-00065 - below + */ + +/** XAPI-00065, Data 2.4.4.2 when the object is an agent or a group + * Statements that use an Agent or Group as an Object MUST specify an "objectType" property. The LRS rejects with 400 Bad Request if the “objectType” property is absent and the Object is an Agent Object or Group Object. + */ + describe('Statements that use an Agent or Group as an Object MUST specify an "objectType" property. (Data 2.4.4.2.s1.b1, XAPI-00065)', function () { + + it('should fail when using agent as object and no objectType', function (done) { + var templates = [ + {statement: '{{statements.object_agent_default}}'} + ]; + var data = helper.createFromTemplate(templates).statement; + delete data.object.objectType; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(400, done); + }); + + it('should fail when using group as object and no objectType', function (done) { + var templates = [ + {statement: '{{statements.object_group_default}}'} + ]; + var data = helper.createFromTemplate(templates).statement; + delete data.object.objectType; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(400, done); + }); + + it('substatement should fail when using agent as object and no objectType', function (done) { + var templates = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{statements.object_agent_default}}'} + ]; + var data = helper.createFromTemplate(templates).statement; + delete data.object.objectType; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(400, done); + }); + + it('substatement should fail when using group as object and no objectType', function (done) { + var templates = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{statements.object_group_default}}'} + ]; + var data = helper.createFromTemplate(templates).statement; + delete data.object.objectType; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(400, done); + }); + }); + + //Data 2.4.4.3 - when the object is a statement +/** Matchup with Conformance Requirements Document + * XAPI-00066 - in substatements.js + * XAPI-00067 - in substatements.js + * XAPI-00068 - in substatements.js + * XAPI-00069 - in substatements.js + * XAPI-00070 - in substatements.js + * XAPI-00071 - in substatements.js + * XAPI-00072 - in statementrefs.js + * XAPI-00073 - in statementrefs.js + */ + templatingSelection.createTemplate('substatements.js'); + templatingSelection.createTemplate('statementrefs.js'); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.2.4-Result-Requirements.js b/test/v2_0/4.2.2.4-Result-Requirements.js new file mode 100644 index 00000000..a8463493 --- /dev/null +++ b/test/v2_0/4.2.2.4-Result-Requirements.js @@ -0,0 +1,35 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Result Property Requirements (Data 2.4.5)', () => { + +/** Matchup with Conformance Requirements Document + * Data 2.4.5 Result + * XAPI-00074 - in results.js + * XAPI-00075 - in results.js + * XAPI-00076 - in results.js + * XAPI-00077 - in results.js + * XAPI-00078 - in results.js + + * Data 2.4.5.1 Score + * XAPI-00079 - in scores.js + * XAPI-00080 - in scores.js + * XAPI-00081 - in scores.js + * XAPI-00082 - in scores.js + * XAPI-00083 - in scores.js + */ + templatingSelection.createTemplate('results.js'); + templatingSelection.createTemplate('scores.js'); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.2.5-Context-Requirements.js b/test/v2_0/4.2.2.5-Context-Requirements.js new file mode 100644 index 00000000..a0eadc4e --- /dev/null +++ b/test/v2_0/4.2.2.5-Context-Requirements.js @@ -0,0 +1,136 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Context Property Requirements (Data 2.4.6)', function () { + + //Data 2.4.6.s3 +/** Matchup with Conformance Requirements Document + * XAPI-00084 - in contexts.js + * XAPI-00085 - in contexts.js + * XAPI-00086 - in contexts.js + * XAPI-00087-1 - in contexts.js + * XAPI-00087-2 - in contexts.js + * XAPI-00088 - in contexts.js + * XAPI-00089 - in contexts.js + * XAPI-00090 - in contexts.js + * XAPI-00091 - in contexts.js + * XAPI-00092 - in contexts.js + */ + templatingSelection.createTemplate('contexts.js'); + + //Data 2.4.6.2 +/** Matchup with Conformance Requirements Document + * XAPI-00093 - in contextactivities.js + * XAPI-00094 - in contextactivities.js + * XAPI-00095 - removed per 02/08/2017 spec call + * XAPI-00096 - below + */ + templatingSelection.createTemplate('contextactivities.js'); + templatingSelection.createTemplate('contextagents.js'); + templatingSelection.createTemplate('contextgroups.js'); + +/** XAPI-00096, Data 2.4.6.2 ContextActivities Property + * An LRS's Statement Resource returns a ContextActivity in an array, even if only a single ContextActivity is returned. + */ + describe('An LRS returns a ContextActivity in an array, even if only a single ContextActivity is returned (Data 2.4.6.2.s4.b3, XAPI-00096)', function () { + var types = ['parent', 'grouping', 'category', 'other']; + this.timeout(0); + + types.forEach(function (type) { + it('should return array for statement context "' + type + '" when single ContextActivity is passed', function (done) { + var templates = [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.' + type + '}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var query = '?statementId=' + data.id; + var stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } + else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, data.id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var statement = helper.parse(res.body, done); + expect(statement).to.have.property('context').to.have.property('contextActivities'); + expect(statement.context.contextActivities).to.have.property(type); + expect(statement.context.contextActivities[type]).to.be.an('array'); + done(); + } + }); + } + }); + }); + }); + + types.forEach(function (type) { + it('should return array for statement substatement context "' + type + '" when single ContextActivity is passed', function (done) { + var templates = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.' + type + '}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var query = '?statementId=' + data.id; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, data.id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var statement = helper.parse(res.body, done); + expect(statement).to.have.property('object').to.have.property('context').to.have.property('contextActivities'); + expect(statement.object.context.contextActivities).to.have.property(type); + expect(statement.object.context.contextActivities[type]).to.be.an('array'); + done(); + } + }); + } + }); + }); + }); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.2.6-Attachment-Requirements.js b/test/v2_0/4.2.2.6-Attachment-Requirements.js new file mode 100644 index 00000000..1473eae5 --- /dev/null +++ b/test/v2_0/4.2.2.6-Attachment-Requirements.js @@ -0,0 +1,30 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Attachments Property Requirements (Data 2.4.11)', function() { + +/** Matchup with Conformance Requirements Document + * XAPI-00102 - in attachments.js + * XAPI-00103 - in attachments.js + * XAPI-00104 - in attachments.js + * XAPI-00105 - in attachments.js + * XAPI-00106 - in attachments.js + * XAPI-00107 - in attachments.js + + * Note XAPI-00025 - in attachments.js + */ + + templatingSelection.createTemplate('attachments.js'); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.4.2-Authority-Requirements.js b/test/v2_0/4.2.4.2-Authority-Requirements.js new file mode 100644 index 00000000..7b604209 --- /dev/null +++ b/test/v2_0/4.2.4.2-Authority-Requirements.js @@ -0,0 +1,88 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Authority Property Requirements (Data 2.4.9)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00098 - in authorities.js + * XAPI-00099 - below + * XAPI-00100 - below + * + * Note - XAPI-00024 - in authorities.js + */ + templatingSelection.createTemplate('authorities.js'); + +/** XAPI-00100, Data 2.4.9 Authority + * An LRS rejects with error code 400 Bad Request, a Request whose "authority" is a Group having more than two Agents + */ + it('An LRS rejects with error code 400 Bad Request, a Request whose "authority" is a Group and consists of non-O-Auth Agents (Data 2.4.9.s3.b3, XAPI-00100)', function (done) { + var templates = [ + {statement: '{{statements.default}}'}, + {authority: {"objectType": "Group", "name": "xAPI Group", "mbox": "mailto:xapigroup@example.com", + "member":[{"name":"agentA","mbox":"mailto:agentA@example.com"},{"name":"agentB","mbox":"mailto:agentB@example.com"}]}} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(400, done) + }); + +/** XAPI-00099, Data 2.4.9 Authority + * An LRS populates the "authority" property if it is not provided in the Statement + */ + describe('An LRS populates the "authority" property if it is not provided in the Statement, based on header information with the Agent corresponding to the user (contained within the header) (Implicit, Data 2.4.9.s3.b4, XAPI-00099) ', function () { + + it('should populate authority ', function (done) { + + this.timeout(0); + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var query = '?statementId=' + data.id; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .headers(helper.addAllHeaders({})) + .wait(helper.genDelay(stmtTime, query, data.id)) + .expect(200).end(function (err, res) { + if (err) { + done(err); + } else { + var statement = helper.parse(res.body, done); + expect(statement).to.have.property('authority'); + done(); + } + }); + } + }) + }); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.4.2-ID-Requirements.js b/test/v2_0/4.2.4.2-ID-Requirements.js new file mode 100644 index 00000000..9d5f2fb2 --- /dev/null +++ b/test/v2_0/4.2.4.2-ID-Requirements.js @@ -0,0 +1,81 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +/** As there is no Data 2.4 file, I will match them up here + * Matchup with Conformance Requirements Document + * XAPI-00021 - these are all in Multiplicity folder, the community said this won't be a problem and do not test it. some are also covered in templating tests, usually in these cases post and 200 or 400. + * XAPI-00022 - in timestamp_property.js + * XAPI-00023 - in Data 2.4.8 Stored Property + * XAPI-00024 - in authorities.js + * XAPI-00025 - in attachments.js + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +/** Matchup with Conformance Requirements Document + * XAPI-00026 - found below + * XAPI-00027 - in uuids.js + * XAPI-00028 - in uuids.js + * XAPI-00029 - in uuids.js + * XAPI-00030 - in uuids.js + */ + +describe('Id Property Requirements (Data 2.4.1)', () => { + + templatingSelection.createTemplate('uuids.js'); + +/** XAPI-00026, Data 2.4.1 Id + * An LRS generates the "id" property of a Statement if none is provided (Modify, 4.1.1.a) + */ + describe ('An LRS generates the "id" property of a Statement if none is provided (Modify, Data 2.4.1.s2.b1, XAPI-00026)', function (){ + + it('should complete an empty id property', (done) => { + this.timeout(0); + var stmtid, query; + var templates = [ + {statement: '{{statements.default}}'} + ]; + data = helper.createFromTemplate(templates); + data = data.statement; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + stmtid = res.body[0]; + query = '?statementId=' + stmtid; + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, stmtid)) + .headers(helper.addAllHeaders({})) + .end(function (err, res) { + if (err) { + done(err); + } else { + var results = helper.parse(res.body, done); + expect(results.id).to.not.be.undefined; + expect(results.id).to.eql(stmtid); + done(); + } + }) + } + }); + }); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.4.2-Stored-Requirements.js b/test/v2_0/4.2.4.2-Stored-Requirements.js new file mode 100644 index 00000000..dee7625a --- /dev/null +++ b/test/v2_0/4.2.4.2-Stored-Requirements.js @@ -0,0 +1,144 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Stored Property Requirements (Data 2.4.8)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00097 - below + * + * Note XAPI-00023 - below + */ + +/** XAPI-00097, Data 2.4.8 Stored + * An LRS MUST assign the "stored" property timestamp upon receiving a statement. + */ + describe('An LRS MUST accept statements with the stored property (Data 2.4.8.s3.b2, XAPI-00097)', function () { + this.timeout(0); + var storedTime = new Date('July 15, 2011').toISOString(); + var template = [ + {statement: '{{statements.default}}'}, + {stored: storedTime} + ]; + var data = helper.createFromTemplate(template).statement; + var postId, putId; + + it('using POST', function (done) { + var stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders()) + .json(data) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + postId = res.body[0]; + var query = '?statementId=' + postId; + + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, postId)) + .headers(helper.addAllHeaders()) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + expect(result).to.have.property('stored'); + var stmtStored = result.stored; + expect(stmtStored).to.not.eql(storedTime); + done(); + } + }); + } + }); + }); + + it('using PUT', function (done) { + putId = helper.generateUUID(); + param = '?statementId=' + putId; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + param) + .headers(helper.addAllHeaders()) + .json(data) + .expect(204) + .end((err, res) => { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + param) + .wait(helper.genDelay(stmtTime, param, putId)) + .headers(helper.addAllHeaders()) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + expect(result).to.have.property('stored'); + var stmtStored = result.stored; + expect(stmtStored).to.not.eql(storedTime); + done(); + } + }); + } + }); + }); + }); + +/** XAPI-00023, 2.4 Statement Properties + * A "stored" property is a TimeStamp, per section 4.5. An LRS assigns the “stored” property upon receipt with a valid TimeStamp. + */ + describe('A stored property must be a TimeStamp (Data 2.4.8.s2, XAPI-00023)', function () { + it('retrieve statements, test a stored property', (done) => { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .headers(helper.addAllHeaders()) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + var stmts = result.statements; + var milliChecker = (num) => { + expect(stmts[num]).to.have.property('stored'); + //formatted iso 8601 + var chkStored = moment(stmts[num].stored, moment.ISO_8601); + expect(chkStored.isValid()).to.be.true; + expect(isNaN(chkStored._pf.parsedDateParts[6])).to.be.false; + //precision to milliseconds + if ((chkStored._pf.parsedDateParts[6] % 10) > 0) { + expect(chkStored._pf.parsedDateParts[6] % 10).to.be.above(0); + done(); + } else { + if (++num < stmts.length) { + milliChecker(num); + } else { + expect(chkStored._pf.parsedDateParts[6] % 10).to.be.above(0); + done(); + } + } + }; milliChecker(0); + } + }); + }); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/4.2.4.2-Timestamp-Requirements.js b/test/v2_0/4.2.4.2-Timestamp-Requirements.js new file mode 100644 index 00000000..12aef769 --- /dev/null +++ b/test/v2_0/4.2.4.2-Timestamp-Requirements.js @@ -0,0 +1,23 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Timestamp Property Requirements (Data 2.4.7)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00022 - in timestamp_property.js + */ + + templatingSelection.createTemplate('timestamp_property.js'); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.4.3-Version-Requirements.js b/test/v2_0/4.2.4.3-Version-Requirements.js new file mode 100644 index 00000000..35ac8454 --- /dev/null +++ b/test/v2_0/4.2.4.3-Version-Requirements.js @@ -0,0 +1,73 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Version Property Requirements (Data 2.4.10)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00101 - in version.js + * Unnumbered test - came from XAPI-00332 + */ + + templatingSelection.createTemplate('version.js'); + +/** XAPI-00332, Communication 3.3 Versioning which should be moved to Data 2.4.10 Version Property + * Statements returned by an LRS MUST retain the version property they are accepted with. + */ + it ('Statements returned by an LRS MUST retain the version property they are accepted with (Format, Data 2.4.10, XAPI-00332)', function (done){ + this.timeout(0); + var stmtTime = Date.now(); + + var statementTemplates = [ + {statement: '{{statements.default}}'} + ]; + + var version = '2.0.0'; + var id = helper.generateUUID(); + + var statement = helper.createFromTemplate(statementTemplates); + statement = statement.statement; + statement.id = id; + statement.version = version; + + var query = helper.getUrlEncoding({statementId: id}); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(statement) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function(err,res){ + if (err){ + done(err); + } + else{ + var results = helper.parse(res.body); + expect(results.version).to.equal(version); + done(); + } + }); + } + }); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('../helper'), require('../multipartParser'), require('../redirect.js'), require('../templatingSelection'))); diff --git a/test/v2_0/4.2.5-Statement-Voiding.js b/test/v2_0/4.2.5-Statement-Voiding.js new file mode 100644 index 00000000..2a7c0160 --- /dev/null +++ b/test/v2_0/4.2.5-Statement-Voiding.js @@ -0,0 +1,236 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Statement Lifecycle Requirements (Data 2.3)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00016 - below + * XAPI-00017 - in voiding.js + * XAPI-00018 - below + * XAPI-00019 - in voiding.js + * XAPI-00020 - in voiding.js + */ + + templatingSelection.createTemplate('voiding.js'); + + /** XAPI-00018, Data 2.3.2 Voiding + * An LRS MUST consider a Statement it contains voided if the Statement is not itself a voiding Statement and the LRS also contains a voiding Statement referring to the first Statement. + * Test: Void a statement and then send a GET for that statement which uses “statementId” instead of “voidedStatementId.” The statement should then not be returned in the GET request, which should return a 404. + */ + describe('A Voided Statement is defined as a Statement that is not a Voiding Statement and is the Target of a Voiding Statement within the LRS (Data 2.3.2.s2.b3, XAPI-00018)', function () { + var voidedId = helper.generateUUID(); + var stmtTime; + + before('Persist voided statement', function (done) { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var voided = helper.createFromTemplate(templates); + voided = voided.statement; + voided.id = voidedId; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voided) + .expect(200, done); + }); + + before('Persist voiding statement', function (done) { + var templates = [ + {statement: '{{statements.voiding}}'} + ]; + var voiding = helper.createFromTemplate(templates); + voiding = voiding.statement; + voiding.object.id = voidedId; + stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(voiding) + .expect(200, done); + }); + + it('Should return a voided statement when using GET "voidedStatementId"', function (done) { + this.timeout(0); + var query = helper.getUrlEncoding({voidedStatementId: voidedId}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var statement = helper.parse(res.body, done); + expect(statement.id).to.equal(voidedId); + done(); + } + }); + }); + + it('Should return 404 when using GET with "statementId"', function (done) { + this.timeout(0); + var query = helper.getUrlEncoding({statementId: voidedId}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(404, done); + }); + }); + + /** XAPI-00016, Data 2.3.2 Voiding + * A Voiding Statement cannot Target another Voiding Statement. + * LRS behavior this new VOIDING statement MAY be rejected. + * If the LRS accepts that statement, the violating VOIDING statement SHOULD be ignored. + * Adjust this test accordingly + */ + describe('A Voiding Statement cannot Target another Voiding Statement (Data 2.3.2.s2.b7, XAPI-00016)', function () { + var voidedId, voidingId; + + before('Persist voided statement', function (done) { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data).expect(200).end(function (err, res) { + if (err) { + done(err); + } else { + voidedId = res.body[0]; + done(); + } + }); + }); + + before('Persist voiding statement', function (done) { + var templates = [ + {statement: '{{statements.voiding}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.object.id = voidedId; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data).expect(200).end(function (err, res) { + if (err) { + done(err); + } else { + voidingId = res.body[0]; + done(); + } + }); + }); + + it('Should not void an already voided statement', function (done) { + this.timeout(0); + var templates = [ + {statement: '{{statements.object_statementref}}'}, + {verb: '{{verbs.voided}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.object.id = voidedId; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data).end(function (err, res) { + if (err) { + done(err); + } else { + var query = '?voidedStatementId=' + voidedId; + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .wait(helper.genDelay(stmtTime, query, voidedId)) + .headers(helper.addAllHeaders({})) + .expect(200, done); + } + }); + }); + + it('Should not void a voiding statement', function (done) { + this.timeout(0); + var templates = [ + {statement: '{{statements.object_statementref}}'}, + {verb: '{{verbs.voided}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.object.id = voidingId; + var stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data).end(function (err, res) { + if (err) { + done(err); + } else { + var query = '?statementId=' + voidingId; + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .headers(helper.addAllHeaders({})) + .wait(helper.genDelay(stmtTime, query, voidingId)) + .expect(200, done); + } + }); + }); + }); + + /** 4.2.4.1 LRS Rejection Cases + * Update for 2.0 + * + * Never reject a stateemnt for using the voided verb. + */ + describe('An LRS SHALL NOT reject a voided statement because it cannot find the ID of the Object of that statement, nor does the LRS have to try to find it. (4.2.4.1 LRS Rejection Cases, XAPI-00016)', function () { + + var nonExistentStatementID = helper.generateUUID(); + + it('Shall not reject a voided statement.', function (done) { + + this.timeout(0); + var templates = [ + {statement: '{{statements.object_statementref}}'}, + {verb: '{{verbs.voided}}'} + ]; + + var data = helper.createFromTemplate(templates); + + data = data.statement; + data.object.id = nonExistentStatementID; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data).expect(200).end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + }); +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/4.2.7-Additional-Requirements-for-Data-Types.js b/test/v2_0/4.2.7-Additional-Requirements-for-Data-Types.js new file mode 100644 index 00000000..d56b564d --- /dev/null +++ b/test/v2_0/4.2.7-Additional-Requirements-for-Data-Types.js @@ -0,0 +1,151 @@ +const request = require('super-request'); +const expect = require('chai').expect; +const helper = require('../helper'); + +const xapiRequests = require("./util/requests"); + +if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe("(4.2.7) Additional Requirements for Data Types", function () { + + describe("IRIs", function() { + + it ("When storing or comparing IRIs, LRSs shall handle them only by " + + "using one or more of the approaches described in 5.3.1 (Simple String Comparison) " + + "and 5.3.2 (Syntax-Based Normalization) of RFC 3987", async() => { + + let slug = helper.generateUUID(); + + let iriA = `http://example.com/path/${slug}`; + let iriB = `http://example.com/path/../${slug}`; + + let statement = helper.buildStatement(); + statement.object.id = iriB; + + await xapiRequests.sendStatement(statement); + + // We have to receive an activity regardless here, as the 2.0 spec requires it etc. + // + let resA = await xapiRequests.getActivityWithIRI(iriA); + let resB = await xapiRequests.getActivityWithIRI(iriB); + + let activityA = resA.data; + let activityB = resB.data; + + let matchesA = activityA.id === iriA; + let matchesB = activityB.id === iriB; + + expect(matchesA || matchesB).to.be.true; + }); + }); + + describe("Duration", function() { + + it("On receiving a Duration with more than 0.01 second precision, the LRS shall not reject the request.", async() => { + + let statement = { + ...helper.buildStatement(), + id: helper.generateUUID(), + result: { + duration: "P1DT12H36M0.12567S" + } + }; + + let res = await xapiRequests.sendStatement(statement); + + expect(res.status).to.eql(200); + }); + + it("On receiving a Duration with more than 0.01 second precision, the LRS may truncate the duration to 0.01 second precision.", async() => { + + let duration = "P1DT12H36M0.12567S"; + let durationTruncated = "P1DT12H36M0.12S"; + let durationRounded = "P1DT12H36M0.13S"; + + let statement = { + ...helper.buildStatement(), + id: helper.generateUUID(), + result: { + duration + } + }; + + let _ = await xapiRequests.sendStatement(statement); + let getRes = await xapiRequests.getStatementExact(statement.id); + + let statementFromLRS = getRes.data; + + expect(statementFromLRS.result).to.not.be.undefined; + expect(statementFromLRS.result.duration).to.not.be.undefined; + + let original = duration; + let received = statementFromLRS.result.duration; + + let matchesOriginal = (received === original); + let matchesTruncation = (received === durationTruncated); + + let matchedExpectedValue = (matchesOriginal || matchesTruncation); + let matchesRounded = (received === durationRounded); + + expect(matchedExpectedValue).to.eql(true, + matchesRounded + ? `Only truncation is allowed, rounding the seconds duration to a different hundredths value is not allowed.` + : `The LRS seems to have changed the duration from ${original} -> ${received}. You may only truncate the seconds down to the hundredths place.` + ); + }); + + it("When comparing Durations (or Statements containing them), any precision beyond 0.01 second precision shall not be included in the comparison.", async() => { + + let durationFull = "P1DT12H36M0.1237S"; + let durationShort = "P1DT12H36M0.12S"; + + let statement = { + ...helper.buildStatement(), + id: helper.generateUUID(), + result: { + duration: durationShort + } + }; + + let boundary = xapiRequests.generateRandomMultipartBoundary(); + + let shortDurationSignedBody = xapiRequests.generateSignedStatementBody(statement, boundary); + let fullDurationSignedBody = shortDurationSignedBody.replace(durationShort, durationFull); + + let res = await xapiRequests.sendSignedStatementBody(fullDurationSignedBody, boundary); + + expect(res.status).to.eql(200, "When comparing a statement to its signature, ensure that the result.duration field is only compared to the truncated hundredths place of the seconds value."); + }); + }); + + describe("Timestamps", function() { + + it ("checks if the LRS converts timestamps to UTC", async() => { + const dateEST = "2023-05-04T12:00-05:00"; + const dateUTC = "2023-05-04T17:00:00.000Z"; + + let id = helper.generateUUID(); + let statement = helper.buildStatement(); + + statement.id = id; + statement.timestamp = dateEST; + + let res = await xapiRequests.sendStatement(statement); + expect(res.status).is.eql(200); + + let getResponse = await xapiRequests.getStatementExact(id); + let statementFromLRS = getResponse.data; + + expect(statementFromLRS).is.not.undefined; + expect(statementFromLRS).is.not.null; + + let timeReceived = Date.parse(statementFromLRS.timestamp); + let timeExpected = Date.parse(dateUTC); + + expect(timeExpected).is.eql(timeReceived, + `Statement retrieved with timestamp: ${statementFromLRS.timestamp}, expected equivalence to ${dateUTC}` + ); + }); + }); +}); \ No newline at end of file diff --git a/test/v2_0/Data2.2-FormattingRequirements.js b/test/v2_0/Data2.2-FormattingRequirements.js new file mode 100644 index 00000000..d8fe2c89 --- /dev/null +++ b/test/v2_0/Data2.2-FormattingRequirements.js @@ -0,0 +1,437 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + + before("Before all tests are run", function (done) { + console.log("Setting up\nAccounting for time differential between test suite and lrs"); + helper.setTimeMargin(done); + }); + +describe('Formatting Requirements (Data 2.2)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00001 - in formatting.js + * XAPI-00002 - below + * XAPI-00003 - in formatting.js + * XAPI-00004 - in formatting.js + * XAPI-00005 - in formatting.js + * XAPI-00006 - in formatting.js + * XAPI-00007 - in formatting.js + * XAPI-00008 - in formatting.js + * XAPI-00009 - in formatting.js + * XAPI-00010 - in formatting.js + * XAPI-00011 - below + * XAPI-00012 - below + * XAPI-00013 - in formatting.js + * XAPI-00014 - below and in verify.js + * XAPI-00015 - in Communication 1.4 - should stay in Comm 1.4 Encoding + */ + + templatingSelection.createTemplate('formatting.js'); + +/** XAPI-00002, Data 2.2 Formatting Requirements + * An LRS stores 32-bit floating point numbers with at least the precision of IEEE 754 + */ + describe('An LRS stores 32-bit floating point numbers with at least the precision of IEEE 754 (Data 2.2.s4.b3, XAPI-00002)', function() { + this.timeout(0); + + it('should pass and keep precision', function(done) { + + var templates = [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + ], + data = helper.createFromTemplate(templates).statement, + id = helper.generateUUID(), + query = '?statementId=' + id, + min = 0.12123434, + raw = 12.125, + max = 45.45, + stmtTime = Date.now(); + + data.id = id; + data.result.score.min = min; + data.result.score.raw = raw; + data.result.score.max = max; + data.result.score.scaled = min; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + var score = helper.parse(res.body).result.score; + expect(score.min).to.eql(min); + expect(score.raw).to.eql(raw); + expect(score.max).to.eql(max); + expect(score.scaled).to.eql(min); + done(); + } + }); + } + }) + }); + }); + +/** XAPI-00012 + * The LRS rejects with error code 400 Bad Request parameter values which do not validate to the same standards required for values of the same types in Statements. + */ + describe('The LRS rejects with error code 400 Bad Request parameter values which do not validate to the same standards required for values of the same types in Statements (Data 2.2.s4.b4, XAPI-00012)', function (done) { + it('should reject when statementId value is invalid', function () { + var query = helper.getUrlEncoding({statementId: 'wrong'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should reject when statementId value is invalid', function () { + var query = helper.getUrlEncoding({voidedStatementId: 'wrong'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should reject when statementId value is invalid', function () { + var query = helper.getUrlEncoding({agent: 'wrong'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should reject when statementId value is invalid', function () { + var query = helper.getUrlEncoding({verb: 'not.a.valid.iri.com/verb'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should reject when statementId value is invalid', function () { + var query = helper.getUrlEncoding({activity: 'not.a.valid.iri.com/activity'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should reject when statementId value is invalid', function () { + var query = helper.getUrlEncoding({registration: 'wrong'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + }); + + +/** XAPI-00014, Data 2.2 Formatting Requirements + * All Objects are well-created JSON Objects (Nature of Binding) + */ + describe('All Objects are well-created JSON Objects (Nature of binding, Data 2.1, XAPI-00014) **Implicit**', function () { + + templatingSelection.createTemplate('verify.js'); + + it('An LRS rejects a not well-created JSON Object', function(done) { + var verbTemplate = 'http://adlnet.gov/expapi/test/unicode/target/'; + var verb = verbTemplate + helper.generateUUID(); + var malformedTemplates = [ + {statement: '{{statements.default}}'} + ]; + var malformed = helper.createFromTemplate(malformedTemplates); + malformed = malformed.statement; + var string = "\"objectType\": \"Agent\""; + malformed.actor.objectType = string; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(malformed) + .expect(400, done) + }); + }); + +/** XAPI-00011, Data 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI values without a scheme. + */ + describe('An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI values without a scheme. (Data 2.2.s4.b1.b8, XAPI-00011)', function(done) + { + // verb id + it('should fail with bad verb id scheme', function () { + var templates = [ + { + statement: '{{statements.default}}' + }]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.verb.id = data.verb.id.replace("http://","") // remove the scheme portion of the IRI + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // openid + it('should fail with bad verb openid scheme', function () { + var templates = [ + { + statement: '{{statements.actor}}' + }]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.actor.openid = "open.id.com/testUser"; + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // account homePage + it('should fail with bad account homePage', function () { + var templates = [ + { + statement: '{{statements.actor}}' + }]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.actor.account = {"homePage": "homePage.com/testUser", "name": "123456"}; + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // object id + it('should fail with bad object id', function () { + var templates = [ + { + statement: '{{statements.default}}' + }]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.object.id = data.object.id.replace("http://","") // remove the scheme portion of the IRI + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // object type + it('should fail with bad object type', function () { + var templates = [ + { + statement: '{{statements.default}}' + }, + { + object: '{{activities.default}}' + } + ]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.object.definition.type = data.object.definition.type.replace("http://","") // remove the scheme portion of the IRI + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // object moreInfo + it('should fail with bad object moreInfo', function () { + var templates = [ + { + statement: '{{statements.default}}' + }, + { + object: '{{activities.default}}' + } + ]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.object.definition.moreInfo = data.object.definition.moreInfo.replace("http://","") // remove the scheme portion of the IRI + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // attachment usageType + it('should fail with attachment bad usageType', function () { + var templates = [ + { + statement: '{{statements.attachment}}' + }, + { + attachments: [{ + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'description': {'en-US': 'A test attachment (description)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }] + } + ]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.attachments[0].usageType = data.attachments[0].usageType.replace("http://","") // remove the scheme portion of the IRI + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // attachment fileUrl + it('should fail with bad attachment fileUrl', function () { + var templates = [ + { + statement: '{{statements.attachment}}' + }, + { + attachments: [{ + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'description': {'en-US': 'A test attachment (description)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }] + } + ]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.attachments[0].fileUrl = data.attachments[0].fileUrl.replace("http://","") // remove the scheme portion of the IRI + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // object definition extension + it('should fail with bad object definition extension', function () { + var templates = [ + { + statement: '{{statements.default}}' + }, + { + object: '{{activities.default}}' + } + ]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.object.definition.extensions = {"not.valid.com/extension": 1234} + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // context extension + it('should fail with bad context extension', function () { + var templates = [ + { + statement: '{{statements.default}}' + }, + { + context: '{{contexts.default}}' + } + ]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.context.extensions["example.com/extension/wrong"] = 1234 + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + + // result extension + it('should fail with bad result extension', function () { + var templates = [ + { + statement: '{{statements.default}}' + }, + { + result: '{{results.default}}' + } + ]; + var data = helper.createFromTemplate(templates).statement; + data.id = helper.generateUUID(); + data.result.extensions["example.com/extension/wrong"] = 1234 + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(400, done); + }); + }); +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/E.Data2.5-RetrievalofStatements.js b/test/v2_0/E.Data2.5-RetrievalofStatements.js new file mode 100644 index 00000000..a2294aae --- /dev/null +++ b/test/v2_0/E.Data2.5-RetrievalofStatements.js @@ -0,0 +1,349 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, validator) { + // "use strict"; + + chai.use(require('chai-things')); + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Retrieval of Statements (Data 2.5)', function () { + +/** Matchup with Conformance Requirements Document + * XAPI-00108 - below + * XAPI-00109 - below + * XAPI-00110 - below + * XAPI-00111 - below + * XAPI-00112 - duplicate of XAPI-00149 Communication 2.1.3 Statements GET + * XAPI-00113 - below + * XAPI-00114 - below + */ + +/** XAPI-00113, Data 2.5 Retrieval of Statements + * An LRS's Statement API, upon processing a successful GET request, will return a single "statements" property and a single "more" property. A single "more" property must be present if there are additional results available. + */ + describe('An LRS\'s Statement API, upon processing a successful GET request, will return a single "statements" property and a single "more" property. (Data 2.5.s2.table1, XAPI-00113)', function () { + + before('guarantee two statements in LRS', function(done) { + var template = [ + {statement: '{{statements.default}}'} + ], + s1 = helper.createFromTemplate(template).statement, + s2 = helper.createFromTemplate(template).statement, + stmts = [s1, s2]; + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(stmts) + .expect(200, done); + }); + + it('will return single statements property and may return', function(done) { + this.timeout(0); + var query = '?limit=1'; + var stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + query) + .wait(helper.genDelay(stmtTime, query, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements'); + expect(result).to.have.property('more'); + done(); + } + }); + }); + + }); + +/** XAPI-00110, Data 2.5 Retrieval of Statements + * A "statements" property is an Array of Statements. Make a GET request which will return at least one statement and confirm the “statements” property is a valid Array of Statements. + */ + describe('A "statements" property is an Array of Statements (Type, Data 2.5.s2.table1.row1, XAPI-00110)', function () { + var statement, substatement, stmtTime; + this.timeout(0); + + before('persist statement', function (done) { + var templates = [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.category}}'}, + {instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:pri@adlnet.gov" + }} + ]; + var data = helper.createFromTemplate(templates); + statement = data.statement; + + //randomize data to prevent old results from breaking assertion logic + statement.context.contextActivities.category.id += helper.generateUUID(); + statement.verb.id += helper.generateUUID(); + statement.actor.mbox = "mailto:" + helper.generateUUID() + "@adlnet.gov"; + statement.context.registration = helper.generateUUID(); + statement.context.instructor.mbox = "mailto:" + helper.generateUUID() + "@adlnet.gov"; + statement.object.id += helper.generateUUID(); + + statement.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/pri'; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(statement) + .expect(200, done); + }); + + before('persist substatement', function (done) { + var templates = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.category}}'}, + {instructor: { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:sub@adlnet.gov" + }} + ]; + var data = helper.createFromTemplate(templates); + substatement = data.statement; + + //randomize data to prevent old results from breaking assertion logic + substatement.verb.id += helper.generateUUID(); + substatement.actor.mbox = "mailto:" + helper.generateUUID() + "@adlnet.gov"; + + substatement.object.verb.id += helper.generateUUID(); + substatement.object.actor.mbox = "mailto:" + helper.generateUUID() + "@adlnet.gov"; + substatement.object.object.id += helper.generateUUID(); + + + + + substatement.object.context.contextActivities.category.id = 'http://www.example.com/test/array/statements/sub'; + stmtTime = Date.now(); + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(substatement) + .expect(200, done); + }); + + it('should return StatementResult with statements as array using GET without "statementId" or "voidedStatementId"', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .wait(helper.genDelay(stmtTime, undefined, undefined)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('statements').to.be.an('array'); + done(); + } + }); + }); + + }); + +/** XAPI-00114, Data 2.5 Retrieval of Statements + * A "statements" property result which is paginated will create a container for each additional page. + */ + it('A "statements" property which is too large for a single page will create a container for each additional page (Data 2.5.s2.table1.row1, XAPI-00114)', function (done){ + this.timeout(0); + var statementTemplates = [ + {statement: '{{statements.default}}'} + ]; + + var statement1 = helper.createFromTemplate(statementTemplates); + statement1 = statement1.statement; + + var statement2 = helper.createFromTemplate(statementTemplates); + statement2 = statement2.statement; + + var query = helper.getUrlEncoding( + {limit:1} + ); + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json([statement1, statement2]) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, null)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } + else { + var results = helper.parse(res.body, done); + expect(results.statements).to.exist; + expect(results.more).to.exist; + done(); + } + }); + } + }); + }); + +/** XAPI-00109, Data 2.5 Retrieval of Statements + * The "more" property is absent or an empty string (no whitespace) if the entire results of the original GET request have been returned. To test make a GET request which will return a known number of statements and check to make sure the LRS either returns an empty string or the more property is absent. + */ + describe('The "more" property is absent or an empty string (no whitespace) if the entire results of the original GET request have been returned. (Data 2.5.s2.table1.row2, XAPI-00109)', function () { + it('should return empty "more" property or no "more" property when all statements returned', function (done) { + var query = helper.getUrlEncoding({verb: 'http://adlnet.gov/expapi/non/existent/344588672021038'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + var passed = false; + + if (result.more === '' || !result.more) + passed = true; + + expect(passed).to.be.true; + done(); + } + }); + }); + }); + +/** XAPI-00108, Data 2.5 Retrieval of Statements + * If not empty, the "more" property's IRL refers to a specific container object corresponding to the next page of results from the original GET request. To test make a GET request which will return a known number of statements and confirm the LRS returns a “more” property which has an IRL with a container of the remaining statements and that the IRL is valid. + */ + describe('If not empty, the "more" property\'s IRL refers to a specific container object corresponding to the next page of results from the orignal GET request (Data 2.5.s2.table1.row2, XAPI-00108)', function () { + it('should return "more" which refers to next page of results', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?limit=1') + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var result = helper.parse(res.body, done); + expect(result).to.have.property('more'); + expect(validator.isURL(result.more, { + protocols: [], + require_tld: false, + require_protocol: false, + require_host: false, + require_valid_protocol: false, + allow_underscores: true, + host_whitelist: false, + host_blacklist: false, + allow_trailing_dot: false, + allow_protocol_relative_urls: true })).to.be.truthy; + request('') + .get(liburl.resolve(res.request.href, result.more)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } + else { + var results2 = helper.parse(res.body, done); + expect(results2.statements).to.exist; + expect(results2.more).to.exist; + done(); + } + }); + } + }); + }); + }); + +/** XAPI-00111, Data 2.5 Retrieval of Statements + * A "more" property's referenced container object follows the same rules as the original GET request, originating with a single "statements" property and a single "more" property. + */ + it('A "more" property\'s referenced container object follows the same rules as the original GET request, originating with a single "statements" property and a single "more" property (Data 2.5.s2.table1.row2, XAPI-00111)', function (done) { + + this.timeout(0); + var verbTemplate = 'http://adlnet.gov/expapi/test/more/target/'; + var id1 = helper.generateUUID(); + var id2 = helper.generateUUID(); + var statementTemplates = [ + {statement: '{{statements.default}}'} + ]; + + var statement1 = helper.createFromTemplate(statementTemplates); + statement1 = statement1.statement; + statement1.verb.id = verbTemplate + "one"; + statement1.id = id1; + + var statement2 = helper.createFromTemplate(statementTemplates); + statement2 = statement2.statement; + statement2.verb.id = verbTemplate + "two"; + statement2.id = id2; + var query = helper.getUrlEncoding( + {limit:1} + ); + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json([statement1, statement2]) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, "?" + query, id2)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } + else { + var results = helper.parse(res.body, done); + request('') + .get(liburl.resolve(res.request.href, results.more)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } + else { + var results2 = helper.parse(res.body, done); + expect(results2.statements).to.exist; + done(); + } + }); + } + }); + } + }); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('validator'))); diff --git a/test/v2_0/E.Data2.6-SignedStatements.js b/test/v2_0/E.Data2.6-SignedStatements.js new file mode 100644 index 00000000..26de1a46 --- /dev/null +++ b/test/v2_0/E.Data2.6-SignedStatements.js @@ -0,0 +1,138 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, Joi, helper, multipartParser) { + "use strict"; + + var expect = chai.expect; + request = helper.OAuthRequest(request); +describe('Signed Statements (Data 2.6)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00115 - below + * XAPI-00116 - below + * XAPI-00117 - below + */ + + describe('LRS must validate and store statement signatures if they are provided (Data 2.6)', function () { + + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + +/** XAPI-00115, Data 2.5 Signed Statements + * A Signed Statement MUST include a JSON web signature (JWS) as defined here: http://tools.ietf.org/html/rfc7515, as an Attachment with a usageType of http://adlnet.gov/expapi/attachments/signature and a contentType of application/octet-stream. The LRS must reject with 400 a statement which has usageType of http://adlnet.gov/expapi/attachments/signature and a contentType of application/octet-stream but does not have a signature attached. + */ + describe("A Signed Statement MUST include a JSON web signature, JWS (Data 2.6.s4.b1, XAPI-00115)", function () { + + it('rejects a signed statement with a malformed signature - bad content type', function (done) { + data.id = helper.generateUUID(); + var options = {attachmentInfo: {contentType: 'text/plain; charset=ascii'}}; + var body = helper.signStatement(data, options); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({'Content-Type': 'multipart/mixed; boundary=' + options.boundary})) + .body(body) + .expect(400, done); + }); + + it('rejects a signed statement with a malformed signature - bad JWS', function (done) { + data.id = helper.generateUUID(); + var options = {breakJson: true}; + var body = helper.signStatement(data, options); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({'Content-Type': 'multipart/mixed; boundary=' + options.boundary})) + .body(body) + .expect(400, done); + }); + }); + +/** XAPI-00116, Data 2.6 Signed Statements + * The JWS signature MUST have a payload of a valid JSON serialization of the complete Statement before the signature was added.The LRS must reject with 400 a statement which does not have a valid JSON serialization. + */ + describe("The JWS signature MUST have a payload of a valid JSON serialization of the complete Statement before the signature was added. (Data 2.6.s4.b3, XAPI-00116)", function () { + it("rejects statement with invalid JSON serialization", function (done) { + data.id = helper.generateUUID(); + var options = {breakJson: true}; + var body = helper.signStatement(data, options); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({'Content-Type': 'multipart/mixed; boundary='+options.boundary})) + .body(body) + .expect(400, done); + }); + }); + +/** XAPI-00117, Data 2.6 Signed Statements + * The JWS signature MUST use an algorithm of "RS256", "RS384", or "RS512". The LRS must reject with 400 a statement which does not use one of these algorithms or does not use one of these algorithms correctly. + */ + describe('The JWS signature MUST use an algorithm of "RS256", "RS384", or "RS512". (Data 2.6.s4.b4, XAPI-00117)', function () { + + it("Accepts signed statement with \"RS256\"", function (done) + { + // sign statement + data.id = helper.generateUUID(); + var options = {}; + var body = helper.signStatement(data, options); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({'Content-Type': 'multipart/mixed; boundary='+options.boundary})) + .body(body) + .expect(200, done); + }); //end it good sig with "RS256" + + it("Accepts signed statement with \"RS384\"", function (done) + { + // sign statement + data.id = helper.generateUUID(); + var options = {algorithm: 'RS384'}; + var body = helper.signStatement(data, options); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({'Content-Type': 'multipart/mixed; boundary='+options.boundary})) + .body(body) + .expect(200, done); + }); //end it good sig with "RS384" + + it("Accepts signed statement with \"RS512\"", function (done) + { + // sign statement + data.id = helper.generateUUID(); + var options = {algorithm: 'RS512'}; + var body = helper.signStatement(data, options); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({'Content-Type': 'multipart/mixed; boundary='+options.boundary})) + .body(body) + .expect(200, done); + }); //end it good sig with "RS512" + + it('Rejects signed statement with another algorithm', function (done) { + data.id = helper.generateUUID(); + var options = {algorithm: 'HS256'}; + var body = helper.signStatement(data, options); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({'Content-Type': 'multipart/mixed; boundary='+options.boundary})) + .body(body) + .expect(400, done); + }); + }); + + }); //end describe statement signatures + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('joi'), require('./../helper'), require('./../multipartParser'))); diff --git a/test/v2_0/E.Data4.0-SpecialDataTypesAndRules.js b/test/v2_0/E.Data4.0-SpecialDataTypesAndRules.js new file mode 100644 index 00000000..004d8b7e --- /dev/null +++ b/test/v2_0/E.Data4.0-SpecialDataTypesAndRules.js @@ -0,0 +1,502 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect, templatingSelection) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Special Data Types and Rules (Data 4.0)', function () { + + //Data 4.1 +/** Matchup with Conformance Requirements Document + * XAPI-00118 - in extensions.js + * XAPI-00119 - below and in extensions.js + * XAPI-00120 - in extensions.js + */ + templatingSelection.createTemplate("extensions.js"); + +/** XAPI-00119, Data 4.1 Extensions + * An Extension can be null, an empty string, objects with nothing in them. The LRS accepts with 200 if a PUT or 204 if a POST an otherwise valid statement which has any extension value including null, an empty string, or an empty object. + * Tests for other emptys and PUT + */ + describe('An Extension can be null, an empty string, objects with nothing in them when using PUT. (Format, Data 4.1, XAPI-00119)', function () { + var NULL_VALUE = {'extensions': {'http://example.com/ex': null}}, + EMPTY_STRING_VALUE = {'extensions': {'http://example.com/ex': ''}}, + EMPTY_OBJECT_VALUE = {'extensions': {'http://example.com/ex': {}}}, + VALID_EXTENSION_EMPTY = {'extensions': {}}; + + it('statement activity extensions can be empty object', function (done) { + var template = [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: VALID_EXTENSION_EMPTY} + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement activity extension values can be empty string', function (done) { + var template = [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: EMPTY_STRING_VALUE} + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement activity extension values can be null', function (done) { + var template = [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: NULL_VALUE} + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement activity extensions can be empty object', function (done) { + var template = [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: EMPTY_OBJECT_VALUE} + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement result extensions can be empty object', function (done) { + var template = [ + {statement: '{{statements.result}}'}, + {result: '{{results.no_extensions}}'}, + VALID_EXTENSION_EMPTY + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement result extension values can be empty string', function (done) { + var template = [ + {statement: '{{statements.result}}'}, + {result: '{{results.no_extensions}}'}, + EMPTY_STRING_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement result extension values can be null', function (done) { + var template = [ + {statement: '{{statements.result}}'}, + {result: '{{results.no_extensions}}'}, + NULL_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement result extension values can be empty object', function (done) { + var template = [ + {statement: '{{statements.result}}'}, + {result: '{{results.no_extensions}}'}, + EMPTY_OBJECT_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement context extensions can be empty object', function (done) { + var template = [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + VALID_EXTENSION_EMPTY + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement context extension values can be empty string', function (done) { + var template = [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + EMPTY_STRING_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement context extension values can be null', function (done) { + var template = [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + NULL_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement context extension values can be empty object', function (done) { + var template = [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + EMPTY_OBJECT_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement activity extensions can be empty object', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: VALID_EXTENSION_EMPTY} + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement activity extension values can be empty string', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: EMPTY_STRING_VALUE} + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement activity extension values can be null', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: NULL_VALUE} + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement activity extension values can be empty object', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: EMPTY_OBJECT_VALUE} + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement result extensions can be empty object', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.no_extensions}}'}, + VALID_EXTENSION_EMPTY + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement result extension values can be empty string', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.no_extensions}}'}, + EMPTY_STRING_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement result extension values can be null', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: NULL_VALUE} + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement result extension values can be empty object', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.no_extensions}}'}, + EMPTY_OBJECT_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement context extensions can be empty object', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + VALID_EXTENSION_EMPTY + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement context extension values can be empty string', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + EMPTY_STRING_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement context extension values can be null', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + NULL_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + it('statement substatement context extension values can be empty object', function (done) { + var template = [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + EMPTY_OBJECT_VALUE + ], + data = helper.createFromTemplate(template).statement; + data.id = helper.generateUUID(); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(204, done); + }); + + }); + + //Data 4.2 +/** Matchup with Conformance Requirements Document + * XAPI-00121 - in languages.js + */ + templatingSelection.createTemplate("languages.js"); + + //Data 4.5 +/** Matchup with Conformance Requirements Document + * XAPI-00122 - below + * XAPI-00123 - in timestamps.js + */ + templatingSelection.createTemplate("timestamps.js"); + +/** XAPI-00122, Data 4.5 ISO 8601 Timestamps + * A Timestamp MUST preserve precision to at least milliseconds (3 decimal points beyond seconds). The LRS accepts a statement with a valid timestamp which has more than 3 decimal points beyond seconds and when recalled it returns at least 3 decimals points beyond seconds. + */ + describe('A Timestamp MUST preserve precision to at least milliseconds, 3 decimal points beyond seconds. (Data 4.5.s1.b3, XAPI-00122)', function () { + + it('retrieve statements, test a timestamp property', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .headers(helper.addAllHeaders()) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + var stmts = result.statements; + var milliChecker = (num) => { + expect(stmts[num]).to.have.property('timestamp'); + //formatted iso 8601 + var chkStored = moment(stmts[num].timestamp, moment.ISO_8601); + expect(chkStored.isValid()).to.be.true; + expect(isNaN(chkStored._pf.parsedDateParts[6])).to.be.false; + //precision to milliseconds + if ((chkStored._pf.parsedDateParts[6] % 10) > 0) { + expect(chkStored._pf.parsedDateParts[6] % 10).to.be.above(0); + done(); + } else { + if (++num < stmts.length) { + milliChecker(num); + } else { + expect(chkStored._pf.parsedDateParts[6] % 10).to.be.above(0); + done(); + } + } + }; milliChecker(0); + } + }); + }); + + it('retrieve statements, test a stored property', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .headers(helper.addAllHeaders()) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + var result = helper.parse(res.body); + var stmts = result.statements; + var milliChecker = (num) => { + expect(stmts[num]).to.have.property('stored'); + //formatted iso 8601 + var chkStored = moment(stmts[num].stored, moment.ISO_8601); + expect(chkStored.isValid()).to.be.true; + expect(isNaN(chkStored._pf.parsedDateParts[6])).to.be.false; + //precision to milliseconds + if ((chkStored._pf.parsedDateParts[6] % 10) > 0) { + expect(chkStored._pf.parsedDateParts[6] % 10).to.be.above(0); + done(); + } else { + if (++num < stmts.length) { + milliChecker(num); + } else { + expect(chkStored._pf.parsedDateParts[6] % 10).to.be.above(0); + done(); + } + } + }; milliChecker(0); + } + }); + }); + }); + + //Data 4.6 +/** Matchup with Conformance Requirements Document + * XAPI-00124 - in durations.js + */ + templatingSelection.createTemplate("durations.js"); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'), require('./../templatingSelection.js'))); diff --git a/test/v2_0/H.Communication1.1-HeadRequestImplementation.js b/test/v2_0/H.Communication1.1-HeadRequestImplementation.js new file mode 100644 index 00000000..c1561218 --- /dev/null +++ b/test/v2_0/H.Communication1.1-HeadRequestImplementation.js @@ -0,0 +1,190 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('HEAD Request Implementation Requirements (Communication 1.1)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00125 - below + * XAPI-00126 - below + */ + +/** XAPI-00126 + * An LRS accepts HEAD requests. + */ + describe('An LRS accepts HEAD requests (Communication 1.1, XAPI-00126)', function () { + + /* This is to be removed in a future version on the specification and is being removed now. + it('should succeed GET about with no body', function () { + return helper.sendRequest('head', helper.getEndpointAbout(), undefined, undefined, 200); + }); + */ + + it('should succeed HEAD activities with no body', function () { + var statement = helper.buildStatement(); + var parameters = { + activityId: statement.object.id + }; + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('head', helper.getEndpointActivities(), parameters, undefined, 200); + }); + }); + + it('should succeed HEAD activities profile with no body', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('head', helper.getEndpointActivitiesProfile(), parameters, undefined, 200); + }); + }); + + it('should succeed HEAD activities state with no body', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('head', helper.getEndpointActivitiesState(), parameters, undefined, 200); + }); + }); + + it('should succeed HEAD agents with no body', function () { + var statement = helper.buildStatement(); + var parameters = { + agent: statement.actor + }; + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('head', helper.getEndpointAgents(), parameters, undefined, 200); + }); + }); + + it('should succeed HEAD agents profile with no body', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function (){ + return helper.sendRequest('head', helper.getEndpointAgentsProfile(), parameters, undefined, 200); + }); + }); + + it('should succeed HEAD statements with no body', function () { + return helper.sendRequest('head', helper.getEndpointStatements(), undefined, undefined, 200); + }); + }); + +/** XAPI-00125 + * An LRS responds to a HEAD request in the same way as a GET request, but without the message-body. This means run ALL GET tests with HEAD + */ + describe('An LRS responds to a HEAD request in the same way as a GET request, but without the message-body (Communication 1.1.s3.b1, XAPI-00125) **This means run ALL GET tests with HEAD**', function () { + + /* This is to be removed in a future version on the specification and is being removed now. + it('should succeed HEAD about with no body', function () { + return helper.sendRequest('head', helper.getEndpointAbout(), undefined, undefined, 200) + .then(function (res) { + expect(Object.keys(res.body)).to.have.length(0); + }); + }); + */ + + it('should succeed HEAD activities with no body', function () { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + var parameters = { + activityId: data.statement.object.id + } + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('head', helper.getEndpointActivities(), parameters, undefined, 200) + .then(function (res) { + expect(Object.keys(res.body)).to.have.length(0); + }); + }); + }); + + it('should succeed HEAD activities profile with no body', function () { + var parameters = helper.buildActivityProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('head', helper.getEndpointActivitiesProfile(), parameters, undefined, 200) + .then(function (res) { + expect(Object.keys(res.body)).to.have.length(0); + }) + }); + }); + + it('should succeed HEAD activities state with no body', function () { + var parameters = helper.buildState(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('head', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + expect(Object.keys(res.body)).to.have.length(0); + }) + }); + }); + + it('should succeed HEAD agents with no body', function () { + return helper.sendRequest('head', helper.getEndpointAgents(), helper.buildAgent(), undefined, 200) + .then(function (res) { + expect(Object.keys(res.body)).to.have.length(0); + }); + }); + + it('should succeed HEAD agents profile with no body', function () { + var parameters = helper.buildAgentProfile(), + document = helper.buildDocument(); + return helper.sendRequest('post', helper.getEndpointAgentsProfile(), parameters, document, 204) + .then(function () { + return helper.sendRequest('head', helper.getEndpointAgentsProfile(), parameters, undefined, 200) + .then(function (res) { + expect(Object.keys(res.body)).to.have.length(0); + }) + }); + }); + + it('should succeed HEAD statements with no body', function () { + var statement = helper.buildStatement(); + return helper.sendRequest('post', helper.getEndpointStatements(), undefined, [statement], 200) + .then(function () { + return helper.sendRequest('head', helper.getEndpointStatements(), undefined, undefined, 200) + .then(function (res) { + expect(Object.keys(res.body)).to.have.length(0); + }) + }); + }); + }); + + it('An LRS accepts HEAD requests without Content-Length headers (Communication 1.1)', function (done) { + + request(helper.getEndpointAndAuth()) + .head(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + + it('An LRS accepts GET requests without Content-Length headers (Communication 1.1)', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .expect(200, done); + }); + +}); + + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/H.Communication1.2-Headers.js b/test/v2_0/H.Communication1.2-Headers.js new file mode 100644 index 00000000..1a08febc --- /dev/null +++ b/test/v2_0/H.Communication1.2-Headers.js @@ -0,0 +1,19 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Headers Requirements (Communication 1.2)', () => { + + + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/H.Communication1.3-AlternateRequestSyntax.js b/test/v2_0/H.Communication1.3-AlternateRequestSyntax.js new file mode 100644 index 00000000..4498a59b --- /dev/null +++ b/test/v2_0/H.Communication1.3-AlternateRequestSyntax.js @@ -0,0 +1,142 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements. + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Alternate Request Syntax Requirements (Communication 1.3)', function () { + +/** XAPI-00336, Communication 1.3 Alternate Request Syntax + * The LRS MUST support the Alternate Request Syntax. + */ +describe('The LRS does NOT allow Alternate Request Syntax in xAPI 2.0', function () { + /** XAPI-00148, Communication 2.1.2 POST Statements + * An LRS accepts a valid POST request containing a GET request returning 200 OK and the StatementResult Object. + */ + it('An LRS rejects POST requests containing method query parameters', function (done) { + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements() + "?method=GET") + .headers(helper.addAllHeaders({})) + .form({limit: 1}) + .expect(400, done)/* .end(function (err, res) { + if (err) { + done(err); + } else { + var results = helper.parse(res.body, done); + expect(results).to.have.property('statements'); + expect(results).to.have.property('more'); + done(); + } + }); */ + }); + + it('An LRS rejects an alternate request syntax not issued as a POST', function () { + var parameters = {method: 'POST'}; + var formBody = helper.buildFormBody(helper.buildStatement()); + return helper.sendRequest('put', helper.getEndpointStatements(), parameters, formBody, 400); + }); + + it('An LRS REJECTS an alternate request syntax PUT issued as a POST', function () { + var parameters = {method: 'PUT'}; + var formBody = { + statementId: helper.generateUUID(), + content: helper.buildStatement() + } + return helper.sendRequest('post', helper.getEndpointStatements(), parameters, helper.getUrlEncoding(formBody), 400); + }); + + it('During an alternate request syntax the LRS treats the listed form parameters, \'Authorization\', \'X-Experience-API-Version\', \'Content-Type\', \'Content-Length\', \'If-Match\' and \'If-None-Match\', as header parameters (Communictation 1.3.s3.b7)', function () { + var parameters = {method: 'PUT'}; + var sID = helper.generateUUID(); + var query = '?statementId=' + sID; + var formBody = { + statementId: sID, + 'X-Experience-API-Version': '0.8', + content: helper.buildStatement() + } + var stmtTime = Date.now(); + return helper.sendRequest('post', helper.getEndpointStatements(), parameters, helper.getUrlEncoding(formBody), 400); + }); + + it('An LRS will reject an alternate request syntax which contains any extra information with error code 400 Bad Request (Communication 1.3.s3.b4)', function () { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + var statement = data.statement; + var sID = helper.generateUUID(); + var parameters = { + method: 'PUT', + statementId: sID + } + var body = { + statementId: sID, + content: statement, + } + + return helper.sendRequest('post', helper.getEndpointStatements(), parameters, helper.getUrlEncoding(body), 400); + }); + + describe('An LRS will reject an alternate request syntax sending content which does not have a form parameter with the name of "content" (Communication 1.3.s3.b4)', function () { + it('will pass PUT with content body which is url encoded', function (done) { + var headers = helper.addAllHeaders({}); + var auth = headers['Authorization']; + var query = helper.getUrlEncoding({method: 'PUT'}); + + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates).statement; + + var form = { + statementId: helper.generateUUID(), + content: JSON.stringify(data), + 'X-Experience-API-Version': '2.0.0', + Authorization: auth + } + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements() + '?' + query) + .headers({'content-type': 'application/x-www-form-urlencoded'}) + .form(form) + .expect(400, done); + }); + + it('will fail PUT with no content body', function () { + var parameters = {method: 'PUT'}; + return helper.sendRequest('post', helper.getEndpointStatements(), parameters, undefined, 400); + }); + + it('will fail PUT with content body which is not url encoded', function (done) { + var headers = helper.addAllHeaders({}); + var query = helper.getUrlEncoding({method: 'PUT'}); + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates).statement; + headers['content-type'] = 'application/x-www-form-urlencoded'; + + var form = { + statementId: helper.generateUUID(), + content: JSON.stringify(data), + 'X-Experience-API-Version': '2.0.0' + } + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements() + '?' + query) + .headers(headers) + .body(JSON.stringify(form)) + .expect(400, done); + }); + }); +}); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/H.Communication1.4-Encoding.js b/test/v2_0/H.Communication1.4-Encoding.js new file mode 100644 index 00000000..78772222 --- /dev/null +++ b/test/v2_0/H.Communication1.4-Encoding.js @@ -0,0 +1,71 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Encoding Requirements (Communication 1.4)', () => { + +/** XAPI-00015, 2.2. Formatting Requirements + * All Strings are encoded and interpreted as UTF-8 + * This req should stay here (Communication 1.4). This is the only place which mentions UTF-8 in the spec, other than Comm 1.3 + */ + it('All Strings are encoded and interpreted as UTF-8 (Communication 1.4.s1.b1, XAPI-00015)', function (done) { + this.timeout(0); + var verbTemplate = 'http://adlnet.gov/expapi/test/unicode/target/'; + var verb = verbTemplate + helper.generateUUID(); + var unicodeTemplates = [ + {statement: '{{statements.unicode}}'} + ]; + + var unicode = helper.createFromTemplate(unicodeTemplates); + unicode = unicode.statement; + unicode.verb.id = verb; + + var query = helper.getUrlEncoding({ + verb: verb + }); + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(unicode) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, "?" + query, null)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + var results = helper.parse(res.body, done); + var languages = results.statements[0].verb.display; + var unicodeConformant = true; + for (var key in languages){ + if (languages[key] !== unicode.verb.display[key]) + unicodeConformant = false; + } + expect(unicodeConformant).to.be.true; + done(); + } + }); + } + }); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/H.Communication2.2-DocumentResources.js b/test/v2_0/H.Communication2.2-DocumentResources.js new file mode 100644 index 00000000..cb10f6ec --- /dev/null +++ b/test/v2_0/H.Communication2.2-DocumentResources.js @@ -0,0 +1,140 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Document Resource Requirements (Communication 2.2)', function () { + +/** Macthup with Conformance Requirements Document + * XAPI-00182 - below + * XAPI-00183 - below + * XAPI-00184 - below + * XAPI-00185 - untestable + * XAPI-00186 - untestable + */ + +/** XAPI-00182, Communication 2.2 Documents Resources + * An LRS makes no modifications to stored data for any rejected request. + */ + it ('An LRS makes no modifications to stored data for any rejected request (Multiple, including Communication 2.1.2.s2.b4, XAPI-00182)', function(done){ + this.timeout(0); + var templates = [ + {statement: '{{statements.default}}'} + ]; + var correct = helper.createFromTemplate(templates); + correct = correct.statement; + var incorrect = extend(true, {}, correct); + + correct.id = helper.generateUUID(); + incorrect.id = helper.generateUUID(); + + incorrect.verb.id = 'should fail'; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json([correct, incorrect]) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?statementId=' + correct.id) + .wait(helper.genDelay(stmtTime, '?statementId=' + correct.id, correct.id)) + .headers(helper.addAllHeaders({})) + .expect(404, done); + } + }); + }); + +/** XAPI-00184, Communication 2.2 Documents Resources + * A Document Merge overwrites any duplicate values from the previous document with the new document. + */ + it('A Document Merge overwrites any duplicate Objects from the previous document with the new document. (Communication 2.2.s7.b1, Communication 2.2.s7.b2, Communication 2.2.s7.b3, XAPI-00184)', function () { + var parameters = helper.buildState(), + document = { + car: 'MKX' + }, + anotherDocument = { + car: 'MKZ' + }; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, anotherDocument, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql({ + car: 'MKZ' + }) + }); + }); + }); + }); + +/** XAPI-00183, Communication 2.2 Documents Resources + * A Document Merge only performs overwrites at one level deep, although the entire object is replaced. + */ + it('A Document Merge only performs overwrites at one level deep, although the entire object is replaced. (Communication 2.2.s7.b1, Communication 2.2.s7.b2, Communication 2.2.s7.b3, XAPI-00183)', function () { + var parameters = helper.buildState(), + document = { + car: { + make: "Ford", + model: "Escape" + }, + driver: "Dale", + series: { + nascar: { + series: "sprint" + } + } + }, + anotherDocument = { + car: { + make: "Dodge", + model: "Ram" + }, + driver: "Jeff", + series: { + nascar: { + series: "nextel" + } + } + }; + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, document, 204) + .then(function () { + return helper.sendRequest('post', helper.getEndpointActivitiesState(), parameters, anotherDocument, 204) + .then(function () { + return helper.sendRequest('get', helper.getEndpointActivitiesState(), parameters, undefined, 200) + .then(function (res) { + var body = res.body; + expect(body).to.eql({ + car: { + make: "Dodge", + model: "Ram" + }, + driver: "Jeff", + series: { + nascar: { + series: "nextel" + } + } + }) + }); + }); + }); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/H.Communication3.2-ErrorCodes.js b/test/v2_0/H.Communication3.2-ErrorCodes.js new file mode 100644 index 00000000..76438953 --- /dev/null +++ b/test/v2_0/H.Communication3.2-ErrorCodes.js @@ -0,0 +1,221 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Error Codes Requirements (Communication 3.2)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00323 - not found yet - An LRS can only reject Statements using the error codes in this specification - what are we to test here?? + * XAPI-00324 - below + * XAPI-00325 - below + * XAPI-00326 - below + * XAPI-00327 - not found yet - An LRS rejects a Statement of insufficient permissions (credentials are valid, but not adequate) with error code 403 Forbidden + * XAPI-00328 - below + * XAPI-00329 - not found yet - An LRS rejects a Statement due to network/server issues with an error code of 500 Internal Server Error + */ + +/** XAPI-00324, Communication 3.2 Error Codes + * An LRS rejects with error code 400 Bad Request any request to an API which uses a parameter not recognized by the LRS + */ + it('An LRS rejects with error code 400 Bad Request any request to an Resource which uses a parameter not recognized by the LRS (Communication 3.2.s2.b1, XAPI-00324)', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?foo=bar') + .headers(helper.addAllHeaders({})) + .expect(400, done) + }); + +/** XAPI-00325, Communication 3.2 Error Codes + * An LRS rejects with error code 400 Bad Request any request to an API which uses a parameter with differing case + */ + describe('An LRS rejects with error code 400 Bad Request any request to an Resource which uses a parameter with differing case (Communication 3.2.s3.b8, XAPI-00325)', function () { + + it('should fail on PUT statement when not using "statementId"', function (done) { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + + var query = helper.getUrlEncoding({StatementId: data.id}); + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(400, done); + }); + + it('should fail on GET statement when not using "statementId"', function (done) { + var query = helper.getUrlEncoding({StatementId: helper.generateUUID()}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "voidedStatementId"', function (done) { + var query = helper.getUrlEncoding({VoidedStatementId: helper.generateUUID()}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "agent"', function (done) { + var templates = [ + {Agent: '{{agents.default}}'} + ]; + var data = helper.createFromTemplate(templates); + + var query = helper.getUrlEncoding(data); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "verb"', function (done) { + var query = helper.getUrlEncoding({Verb: 'http://adlnet.gov/expapi/verbs/attended'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "activity"', function (done) { + var query = helper.getUrlEncoding({Activity: 'http://www.example.com/meetings/occurances/34534'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "registration"', function (done) { + var query = helper.getUrlEncoding({Registration: 'ec531277-b57b-4c15-8d91-d292c5b2b8f7'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "related_activities"', function (done) { + var query = helper.getUrlEncoding({Related_Activities: true}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "related_agents"', function (done) { + var query = helper.getUrlEncoding({Related_Agents: true}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "since"', function (done) { + var query = helper.getUrlEncoding({Since: '2012-06-01T19:09:13.245Z'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "until"', function (done) { + var query = helper.getUrlEncoding({Until: '2012-06-01T19:09:13.245Z'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "limit"', function (done) { + var query = helper.getUrlEncoding({Limit: 10}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "format"', function (done) { + var query = helper.getUrlEncoding({Format: 'ids'}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "attachments"', function (done) { + var query = helper.getUrlEncoding({Attachments: true}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + + it('should fail on GET statement when not using "ascending"', function (done) { + var query = helper.getUrlEncoding({Ascending: true}); + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .headers(helper.addAllHeaders({})) + .expect(400, done); + }); + }); + +/** XAPI-00326, Communication 3.2 Error Codes + * An LRS rejects with a 400 Bad Request any batch of Statements in which one or more Statements is rejected and if necessary, restores the LRS to the state in which it was before the batch began processing. The response may identify the first statementId which failed. + */ + describe('An LRS does not process any batch of Statements in which one or more Statements is rejected and if necessary, restores the LRS to the state in which it was before the batch began processing (Communication 3.2.s3.b9, XAPI-00326, **Implicit**)', function () { + + it('should not persist any statements on a single failure', function (done) { + this.timeout(0); + var templates = [ + {statement: '{{statements.default}}'} + ]; + var correct = helper.createFromTemplate(templates); + correct = correct.statement; + var incorrect = extend(true, {}, correct); + + correct.id = helper.generateUUID(); + incorrect.id = helper.generateUUID(); + + incorrect.verb.id = 'should fail'; + var query = '?statementId=' + correct.id; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json([correct, incorrect]) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?statementId=' + correct.id) + .wait(helper.genDelay(stmtTime, query, correct.id)) + .headers(helper.addAllHeaders({})) + .expect(404, done); + } + }); + }); + }); + + +/** XAPI-00328, Communication 3.2 Error Codes + * An LRS rejects a Statement due to size if the Statement exceeds the size limit the LRS is configured to with error code 413 Request Entity Too Large. + * Held out for now. No upper limit constraint. + */ +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/H.Communication3.3-Versioning.js b/test/v2_0/H.Communication3.3-Versioning.js new file mode 100644 index 00000000..f15bc771 --- /dev/null +++ b/test/v2_0/H.Communication3.3-Versioning.js @@ -0,0 +1,213 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function (module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) { + // "use strict"; + + var expect = chai.expect; + if(global.OAUTH) + request = helper.OAuthRequest(request); + +describe('Versioning Requirements (Communication 3.3)', () => { + +/** Matchup with Conformance Requirements Document + * XAPI-00330 - below + * XAPI-00331 - below + * XAPI-00332 - in Data 2.4.10 Statements Version Property + * XAPI-00333 - below + */ + +/** XAPI-00333, Communication 3.3 Versioning + * An LRS sends a header response with "X-Experience-API-Version" as the name and latest patch version after 1.0.0 as the value + */ + it ('An LRS sends a header response with "X-Experience-API-Version" as the name and the latest patch version after "1.0.0" as the value (Format, Communication 3.3.s3.b1, Communication 3.3.s3.b2, XAPI-00333)', function (done){ + this.timeout(0); + var id = helper.generateUUID(); + var statementTemplates = [ + {statement: '{{statements.default}}'} + ]; + + var statement = helper.createFromTemplate(statementTemplates); + statement = statement.statement; + statement.id = id; + var query = helper.getUrlEncoding({statementId: id}); + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(statement) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?' + query) + .wait(helper.genDelay(stmtTime, '?' + query, id)) + .headers(helper.addAllHeaders({})) + .expect(200) + .end(function(err,res){ + if (err){ + done(err); + } + else{ + expect(res.headers).to.have.property('x-experience-api-version'); + expect(res.headers['x-experience-api-version']).to.equal(helper.getXapiVersion()); + done(); + } + }); + } + }); + }); + +/** XAPI-00330, Communication 3.3 Versioning + * An LRS will not modify Statements based on a "version" before "1.0.1" + */ + describe('An LRS will not modify Statements based on a "version" before "1.0.1" (Communication 3.3.s3.b4, XAPI-00330)', function () { + it('should not convert newer version format to prior version format', function (done) { + this.timeout(0); + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var query = '?statementId=' + data.id; + var stmtTime = Date.now(); + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?statementId=' + data.id) + .wait(helper.genDelay(stmtTime, query, data.id)) + .headers(helper.addAllHeaders({})) + .expect(200).end(function (err, res) { + if (err) { + done(err); + } else { + var statement = helper.parse(res.body, done); + expect(helper.isEqual(data.actor, statement.actor)).to.be.true; + expect(helper.isEqual(data.object, statement.object)).to.be.true; + expect(helper.isEqual(data.verb, statement.verb)).to.be.true; + done(); + } + }); + } + }); + }); + }); + +/** XAPI-00331, Communication 3.3 Versioning + * An LRS rejects with error code 400 Bad Request, a Request which the "X-Experience-API-Version" header's value + * is anything but "2.0" or "2.0.x", where x is the semantic versioning number to any API except the About API. + */ + describe('An LRS rejects with error code 400 Bad Request, a Request which does not use a "X-Experience-API-Version" header name to any Resource except the About Resource (Format, Communication 3.3.s4.b1, Communication 3.3.s3.b7, Communication 2.8.s5.b4, XAPI-00331)', function () { + + it('Should pass when About GET without header "X-Experience-API-Version"', function (done) { + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointAbout()) + .expect(200, done); + }); + + it('Should fail when Statement GET without header "X-Experience-API-Version"', function (done) { + var stmtId = helper.generateUUID(); + before('Placing the statement to be gotten', function (done) { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates).statement; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + stmtId) + .headers(helper.addAllHeaders({})) + .json(data) + .expect(200, done); + }); + + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements() + '?statementId=' + stmtId) + .headers(helper.addBasicAuthenicationHeader({})) + .end(function (err, res) { + if (err) { + done(err); + } else if (res.statusCode === 400) { + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.0$|^0\.95?$/) + done(); + } else if (res.statusCode === 404) { + expect(res.headers['x-experience-api-version']).to.match(/^0\.95?$/); + done(); + } else { + throw new Error(`Version header (${res.headers['x-experience-api-version']}) and Status Code (${res.statusCode}) do not match specification. Expected version header 2.0.0 with status code 400 or version header 0.9 or 0.95 with status code either 400 or 404.`); + done(); + } + }); + }); + + it('Should fail when Statement POST without header "X-Experience-API-Version"', function (done) { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + + request(helper.getEndpointAndAuth()) + .post(helper.getEndpointStatements()) + .headers(helper.addBasicAuthenicationHeader({})) + .json(data) + .end(function(err, res) { + if (err) { + done(err); + } else if (res.statusCode === 400) { + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.0$|^0\.95?$/); + done(); + } else if (res.statusCode === 404) { + expect(res.headers['x-experience-api-version']).to.match(/^0\.95?$/); + done(); + } else { + throw new Error(`Version header (${res.headers['x-experience-api-version']}) and Status Code (${res.statusCode}) do not match specification. Expected version header 2.0.0 with status code 400 or version header 0.9 or 0.95 with status code either 400 or 404.`); + done(); + } + }); + }); + + it('Should fail when Statement PUT without header "X-Experience-API-Version"', function (done) { + var templates = [ + {statement: '{{statements.default}}'} + ]; + var data = helper.createFromTemplate(templates); + data = data.statement; + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + helper.generateUUID()) + .headers(helper.addBasicAuthenicationHeader({})) + .json(data) + .end(function(err, res) { + if (err) { + done(err); + } else if (res.statusCode === 400) { + expect(res.headers['x-experience-api-version']).to.match(/^2\.0\.0$|^0\.95?$/); + done(); + } else if (res.statusCode === 404) { + expect(res.headers['x-experience-api-version']).to.match(/^0\.95?$/); + done(); + } else { + throw new Error(`Version header (${res.headers['x-experience-api-version']}) and Status Code (${res.statusCode}) do not match specification. Expected version header 2.0.0 with status code 400 or version header 0.9 or 0.95 with status code either 400 or 404.`); + done(); + } + }); + }); + }); + +}); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/H.Communication4.0-Authentication.js b/test/v2_0/H.Communication4.0-Authentication.js new file mode 100644 index 00000000..aa81cd5b --- /dev/null +++ b/test/v2_0/H.Communication4.0-Authentication.js @@ -0,0 +1,153 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xapi-lrs-conformance-requirements + */ + +(function(module, fs, extend, moment, request, requestPromise, chai, liburl, Joi, helper, multipartParser, redirect) +{ + + describe('Authentication Requirements (Communication 4.0)', function() + { + + /** XAPI-00334, Communication 2.1.3 GET Statements + * An LRS rejects a Statement of bad authorization (either authentication needed or failed credentials) with error code 401 Unauthorized + */ + describe('An LRS rejects a Statement of bad authorization, either authentication needed or failed credentials, with error code 401 Unauthorized (Authentication, Communication 4.0, XAPI-00334)', function() + { + // This test was not allowing for different, non-standardized prioritizations of request statuses + // when rejecting Requests based on Authentication. An LRS may receive a request with bad credentials, + // but place higher priority on an improper header -- returning 400 for that header violation. This test + // has been updated to first check that bad credentials receive a 401 and then check that bad credentials + // AND invalid headers receive either 400 or 401. + // + // Identical changes were made to the 1.0.2 test for Authentication in v1_0_2/non_templating.js. + + it("fails when given a random name pass pair", function (done) { + if (global.OAUTH) { + done(); + } + else { + var templates = [ + { + statement: '{{statements.default}}' + }]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var headers = helper.addAllHeaders({}); + + //warning: this ".\\" is super important. Node caches the modules, and the superrequest module has been modified to work correctly + //with oauth already. We get a new verions by appending some other characters to defeat the cache. + if (global.OAUTH) + request = require('.\\super-request'); + else + headers["Authorization"] = 'Basic ' + new Buffer('RobCIsNot:AUserOnThisLRS123').toString('base64'); + + // Assuming everything is fine, minus the auth credentials + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(401) + .end(); + + // In the case of BOTH a bad header situation AND bad auth, the LRS can return either 401 or 400 + headers["X-Experience-API-Version"] = "BAD"; + + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .headers(headers) + .end(function (err, res) { + + if (res.statusCode === 400 || res.statusCode === 401) { + done(); + } else { + done("Response should have been either 401 or 400."); + } + + }); + } + }); + + it('fails with a malformed header', function (done) { + if (global.OAUTH) { + done(); + } + else { + var templates = [ + { + statement: '{{statements.default}}' + }]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var headers = helper.addAllHeaders( + {}); + + //warning: this ".\\" is super important. Node caches the modules, and the superrequest module has been modified to work correctly + //with oauth already. We get a new verions by appending some other characters to defeat the cache. + if (global.OAUTH) + request = require('.\\super-request'); + else + headers["Authorization"] = 'Basic:' + new Buffer('RobCIsNot:AUserOnThisLRS').toString('base64'); //note bad encoding here. + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(401) + .end(); + + // Same as above + headers["X-Experience-API-Version"] = "BAD"; + + request(helper.getEndpointAndAuth()) + .get(helper.getEndpointStatements()) + .headers(headers) + .end(function (err, res) { + + if (res.statusCode === 400 || res.statusCode === 401) { + done(); + } else { + done("Response should have been either 401 or 400."); + } + + }); + } + }); + }); + + + /** XAPI-00335, Communication 2.1.3 GET Statements + * An LRS must support HTTP Basic Authentication + */ + //WARNING: This might not be a great test. OAUTH will override it + it('An LRS must support HTTP Basic Authentication (Authentication, Communication 4.0, XAPI-00335)', function(done) + { + if (global.OAUTH) + { + done(); + } + else + { + var templates = [ + { + statement: '{{statements.default}}' + }]; + var data = helper.createFromTemplate(templates); + data = data.statement; + data.id = helper.generateUUID(); + var headers = helper.addAllHeaders( + {}); + + request(helper.getEndpointAndAuth()) + .put(helper.getEndpointStatements() + '?statementId=' + data.id) + .headers(headers) + .json(data) + .expect(204, done); + } + }); + + }); + +}(module, require('fs'), require('extend'), require('moment'), require('super-request'), require('supertest-as-promised'), require('chai'), require('url'), require('joi'), require('./../helper'), require('./../multipartParser'), require('./../redirect.js'))); diff --git a/test/v2_0/configs/accountobjects.js b/test/v2_0/configs/accountobjects.js new file mode 100644 index 00000000..1d4a38d5 --- /dev/null +++ b/test/v2_0/configs/accountobjects.js @@ -0,0 +1,431 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_OBJECT = {key: 'value'}; + var INVALID_OBJECTTYPE_NUMERIC = {objectType: 123}; + var INVALID_OBJECTTYPE_OBJECT = {objectType: INVALID_OBJECT}; + var INVALID_OBJECTTYPE_NAME_NUMERIC = {name: 123}; + var INVALID_OBJECTTYPE_NAME_OBJECT = {name: INVALID_OBJECT}; + var INVALID_MAIL_TO_EMAIL = 'mailto:should.fail.com'; + var INVALID_MAIL_TO_IRI = 'http://should.fail.com'; + var INVALID_URI = 'ab=c://should.fail.com'; + var INVALID_ACCOUNT_HOMEPAGE_IRL = {account: {homePage: INVALID_URI}}; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00042, 2.4.2.4 Account Object + * An Account Object's homePage" property is an IRL. An LRS rejects with 400 Bad Request if a statement uses the “account” IFI and the “homePage” property is absent or has an invalid IRL. + */ + name: 'An Account Object uses the "homePage" property (Multiplicity, Data 2.4.2.4.s2.table1.row1, XAPI-00042)', + config: [ + { + name: 'statement actor "agent" account "homePage" property exists', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement actor "group" account "homePage" property exists', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement authority "agent" account "homePage" property exists', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement authority "group" account "homePage" property exists', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement context instructor "agent" account "homePage" property exists', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement context instructor "group" account "homePage" property exists', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement context team "group" account "homePage" property exists', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement substatement as "agent" account "homePage" property exists', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement substatement as "group" account "homePage" property exists', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s "agent" account "homePage" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s "group" account "homePage" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "agent" account "homePage" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "group" account "homePage" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_homepage}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "group" account "homePage" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_homepage}}'} + ], + expect: [400] + } + ] + }, + { //see above + name: 'An Account Object\'s "homePage" property is an IRL (Type, Data 2.4.2.4.s2.table1.row1, XAPI-00042)', + config: [ + { + name: 'statement actor "agent" account "homePage property is IRL', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement actor "group" account "homePage property is IRL', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement authority "agent" account "homePage property is IRL', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement authority "group" account "homePage property is IRL', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement context instructor "agent" account "homePage property is IRL', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement context instructor "group" account "homePage property is IRL', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement context team "group" account "homePage property is IRL', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement substatement as "agent" account "homePage property is IRL', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement substatement as "group" account "homePage property is IRL', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s "agent" account "homePage property is IRL', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s "group" account "homePage property is IRL', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "agent" account "homePage property is IRL', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "group" account "homePage property is IRL', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "group" account "homePage property is IRL', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_homepage}}'}, + INVALID_ACCOUNT_HOMEPAGE_IRL + ], + expect: [400] + } + ] + }, + { + /** XAPI-00043, 2.4.2.4 Account Object + * An Account Object "name" property is a String. An LRS rejects with 400 Bad Request if a statement uses the “account” IFI and the “name” property is absent or has an invalid string. + */ + name: 'An Account Object uses the "name" property (Multiplicity, Data 2.4.2.4.s2.table1.row2, XAPI-00043)', + config: [ + { + name: 'statement actor "agent" account "name" property exists', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement actor "group" account "name" property exists', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement authority "agent" account "name" property exists', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement authority "group" account "name" property exists', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement context instructor "agent" account "name" property exists', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement context instructor "group" account "name" property exists', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement context team "group" account "name" property exists', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement substatement as "agent" account "name" property exists', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement substatement as "group" account "name" property exists', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s "agent" account "name" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s "group" account "name" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "agent" account "name" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "group" account "name" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_name}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "group" account "name" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_name}}'} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/activities.js b/test/v2_0/configs/activities.js new file mode 100644 index 00000000..6b9155b6 --- /dev/null +++ b/test/v2_0/configs/activities.js @@ -0,0 +1,554 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_IRI = 'ab=c://should.fail.com'; + var INVALID_NUMERIC = 12345; + var INVALID_OBJECT = {something: 'value'}; + var INVALID_STRING = 'should error'; + var VALID_ACTIVITY = {id: 'http://www.example.com/meetings/occurances/34534'}; + var VALID_EXTENSION_COMPONENT = { + 'id': 'valid', + 'description': { + 'en-US': 'valid' + } + } + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00047, Data 2.4.4.1 when the objectType is activity + * An "object" property uses the "id" property exactly one time. The LRS must reject with 400 Bad Request an otherwise legal statement if the object's objectType is Activity and the object's “id” is not an IRI or the object’s “id” is absent + */ + name: 'An "object" property uses the "id" property exactly one time (Multiplicity, Data 2.4.4.1.s1.table1.row2, XAPI-00047)', + config: [ + { + name: 'statement activity "id" not provided', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_id}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "id" not provided', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_id}}'} + ], + expect: [400] + } + ] + }, + { //see above + name: 'An "object" property\'s "id" property is an IRI (Type, Data 2.2.4.4.1.table1.row2, XAPI-00047)', + config: [ + { + name: 'statement activity "id" not IRI', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'}, + {id: INVALID_IRI} + ], + expect: [400] + }, + { + name: 'statement activity "id" is IRI', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "id" not IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'}, + {id: INVALID_IRI} + ], + expect: [400] + }, + { + name: 'statement substatement activity "id" is IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'} + ], + expect: [200] + } + ] + }, + { + /** XAPI-00048, Data 2.4.4.1 when objectType is activity + * An "object" property uses the "definition" property at most one time. The LRS rejects with 400 Bad Request an otherwise legal statement if the object's "definition" property is an invalid object. + */ + name: 'An Activity\'s "definition" property is an Object (Type, Data 2.4.4.1.s1.table1.row3, XAPI-00048)', + config: [ + { + name: 'statement activity "definition" not object', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement substatement activity "definition" not object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: INVALID_STRING} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00056, Data 2.4.4.1 when objectType is activity + * An Activity Definition's "name" property is a Language Map. The LRS rejects with 400 Bad Request an otherwise legal statement if the Activity Definition's "name" property is present and is an invalid Language Map. + */ + name: 'An Activity Definition\'s "name" property is a Language Map (Type, Data 2.4.4.1.s2.table1.row1, XAPI-00056)', + config: [ + { + name: 'statement object "name" language map is numeric', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.numeric_name}}'} + ], + expect: [400] + }, + { + name: 'statement object "name" language map is string', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.string_name}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "name" language map is numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.numeric_name}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "name" language map is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.string_name}}'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00059, Data 2.4.4.1 when objectType is activity + * An Activity Definition's "description" property is a Language Map. The LRS rejects with 400 Bad Request an otherwise legal statement if the Activity Definition's "description" property is present and is an invalid Language Map. + */ + name: 'An Activity Definition\'s "description" property is a Language Map (Type, Data 2.4.4.1.s2.table1.row2, XAPI-00059)', + config: [ + { + name: 'statement object "description" language map is numeric', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.numeric_description}}'} + ], + expect: [400] + }, + { + name: 'statement object "description" language map is string', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.string_description}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "description" language map is numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.numeric_description}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "description" language map is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.string_description}}'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00060, Data 2.4.4.1 when objectType is activity + * An Activity Definition's "type" property is an IRI. The LRS rejects with 400 Bad Request an otherwise legal statement if the Activity Definition's "type" property is present and is an invalid IRI. + */ + name: 'An Activity Definition\'s "type" property is an IRI (Type, Data 2.4.4.1.s2.table1.row3, XAPI-00060)', + config: [ + { + name: 'statement activity "type" not IRI', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {type: INVALID_STRING}} + ], + expect: [400] + }, + { + name: 'statement substatement activity "type" not IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {type: INVALID_STRING}} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00061, Data 2.4.4.1 when objectType is activity + * An Activity Definition's "moreinfo" property is an IRL. The LRS rejects with 400 Bad Request an otherwise legal statement if the Activity Definition's "moreinfo" property is present and is an invalid IRL. + */ + name: 'An Activity Definition\'s "moreinfo" property is an IRL (Type, Data 2.4.4.1.s2.table1.row4, XAPI-00061)', + config: [ + { + name: 'statement activity "moreInfo" not IRI', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {moreInfo: INVALID_STRING}} + ], + expect: [400] + }, + { + name: 'statement substatement activity "moreInfo" not IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {moreInfo: INVALID_STRING}} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00049, Data 2.4.4.1 when objectType is activity + * An Activity Definition's "interactionType" property is a String with a value of either true-false, choice, fill-in, long-fill-in, matching, performance, sequencing, likert, numeric or other. An LRS rejects with 400 Bad Request an Activity Definition's "interactionType" property if it is not a string value of true-false, choice, fill-in, long-fill-in, matching, performance, sequencing, likert, numeric or other. + */ + name: 'An Activity Definition\'s "interactionType" property is a String with a value of either “true-false”, “choice”, “fill-in”, “long-fill-in”, “matching”, “performance”, “sequencing”, “likert”, “numeric” or “other” (Data 2.4.4.1.s8.table1.row1, XAPI-00049)', + config: [ + { + name: 'statement activity "interactionType" can be used with "true-false"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.true_false}}'} + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" can be used with "choice"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.choice}}'} + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" can be used with "fill-in"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.fill_in}}'} + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" can be used with "long-fill-in"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.long_fill_in}}'} + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" can be used with "matching"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.matching}}'} + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" can be used with "performance"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.performance}}'} + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" can be used with "sequencing"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.sequencing}}'} + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" can be used with "likert"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.likert}}'} + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" can be used with "numeric"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.numeric}}'} + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" can be used with "other"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.other}}'}, + ], + expect: [200] + }, + { + name: 'statement activity "interactionType" fails with invalid iri', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.interaction_type_invalid_iri}}'}, + ], + expect: [400] + }, + { + name: 'statement activity "interactionType" fails with invalid numeric', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.interaction_type_invalid_numeric}}'}, + ], + expect: [400] + }, + { + name: 'statement activity "interactionType" fails with invalid object', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.interaction_type_invalid_object}}'}, + ], + expect: [400] + }, + { + name: 'statement activity "interactionType" fails with invalid string', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.interaction_type_invalid_string}}'}, + ], + expect: [400] + }, + { + name: 'statement substatement activity "interactionType" can be used with "true-false"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.true_false}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" can be used with "choice"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.choice}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" can be used with "fill-in"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.fill_in}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" can be used with "long-fill-in"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.long_fill_in}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" can be used with "matching"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.matching}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" can be used with "performance"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.performance}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" can be used with "sequencing"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.sequencing}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" can be used with "likert"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.likert}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" can be used with "numeric"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.numeric}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" can be used with "other"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.other}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "interactionType" fails with invalid iri', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.interaction_type_invalid_iri}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "interactionType" fails with invalid numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.interaction_type_invalid_numeric}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "interactionType" fails with invalid object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.interaction_type_invalid_object}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "interactionType" fails with invalid string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.interaction_type_invalid_string}}'} + ], + expect: [400] + } + ] + }, + + { + /** XAPI-00057, Data 2.4.4.1 when objectType is activity + * An Activity Definition's "extension" property is an Object. The LRS rejects with 400 Bad Request an otherwise legal statement if the Activity Definition's "extension" property is present and is an invalid Extension Object. + */ + name: 'An Activity Definition\'s "extension" property is an Object (Type, Data 2.4.4.1.s2.table1.row5, XAPI-00057)', + config: [ + { + name: 'statement activity "extension" invalid string', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {extensions: INVALID_STRING}} + ], + expect: [400] + }, + { + name: 'statement activity "extension" invalid iri', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {extensions: VALID_EXTENSION_COMPONENT}} + ], + expect: [400] + }, + { + name: 'statement substatement activity "extension" invalid string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {extensions: INVALID_STRING}} + ], + expect: [400] + }, + { + name: 'statement substatement activity "extension" invalid iri', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {extensions: VALID_EXTENSION_COMPONENT}} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/actors.js b/test/v2_0/configs/actors.js new file mode 100644 index 00000000..f7e28cda --- /dev/null +++ b/test/v2_0/configs/actors.js @@ -0,0 +1,144 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_OBJECTTYPE_INVALID_AGENT = {objectType: 'agent'}; + var INVALID_OBJECTTYPE_INVALID_GROUP = {objectType: 'group'}; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00031, Data 2.4.2 Actor + * An "actor" property's "objectType" property is either "Agent" or "Group" An LRS rejects with 400 Bad Request an actor with an “objectType” which is not “Agent” or “Group” + */ + name: 'An "actor" property\'s "objectType" property is either "Agent" or "Group" (Vocabulary, Data 2.4.2.1, Data 2.4.2.2, XAPI-00031)', + config: [ + { + name: 'statement actor "objectType" should fail when not "Agent"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_AGENT + ], + expect: [400] + }, + { + name: 'statement actor "objectType" should fail when not "Group"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_GROUP + ], + expect: [400] + }, + { + name: 'statement authority "objectType" should fail when not "Agent"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_AGENT + ], + expect: [400] + }, + { + name: 'statement authority "objectType" should fail when not "Group"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_GROUP + ], + expect: [400] + }, + { + name: 'statement context instructor "objectType" should fail when not "Agent"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_AGENT + ], + expect: [400] + }, + { + name: 'statement context instructor "objectType" should fail when not "Group"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_GROUP + ], + expect: [400] + }, + { + name: 'statement substatement as agent with "objectType" should fail when not "Agent"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_AGENT + ], + expect: [400] + }, + { + name: 'statement substatement as group with "objectType" should fail when not "Group"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_GROUP + ], + expect: [400] + }, + { + name: 'statement substatement"s actor "objectType" should fail when not "Agent"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_AGENT + ], + expect: [400] + }, + { + name: 'statement substatement"s actor "objectType" should fail when not "Group"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_GROUP + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "objectType" should fail when not "Agent"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_AGENT + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "objectType" should fail when not "Group"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_INVALID_GROUP + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/agents.js b/test/v2_0/configs/agents.js new file mode 100644 index 00000000..e9b7f3e0 --- /dev/null +++ b/test/v2_0/configs/agents.js @@ -0,0 +1,1326 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_OBJECT = {key: 'value'}; + var INVALID_OBJECTTYPE_NUMERIC = {objectType: 123}; + var INVALID_OBJECTTYPE_OBJECT = {objectType: INVALID_OBJECT}; + var INVALID_OBJECTTYPE_NAME_NUMERIC = {name: 123}; + var INVALID_OBJECTTYPE_NAME_OBJECT = {name: INVALID_OBJECT}; + var FOREIGN_IDENTIFIER_ACCOUNT = {'account': {'homePage': 'http://www.example.com', 'name': 'xAPI account name'}}; + var FOREIGN_IDENTIFIER_MBOX = {'mbox': 'mailto:xapi@adlnet.gov'}; + var FOREIGN_IDENTIFIER_MBOX_SHA1SUM = {'mbox_sha1sum': 'cd9b00a5611f94eaa7b1661edab976068e364975'}; + var FOREIGN_IDENTIFIER_OPENID = {'openid': 'http://openid.example.org/12345'}; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00032, Data 2.4.2.1 when the actor objectType is agent + * An "objectType" property is a String. If present, the LRS must validate and reject with 400 Bad Request if invalid + */ + name: 'An "objectType" property is a String (Type, Data 2.4.2.1.s2.table1.row1, XAPI-00032)', + config: [ + { + name: 'statement actor "objectType" should fail numeric', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NUMERIC + ], + expect: [400] + }, + { + name: 'statement actor "objectType" should fail object', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_OBJECT + ], + expect: [400] + }, + { + name: 'statement authority "objectType" should fail numeric', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NUMERIC + ], + expect: [400] + }, + { + name: 'statement authority "objectType" should fail object', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.default}}'}, + INVALID_OBJECTTYPE_OBJECT + ], + expect: [400] + }, + { + name: 'statement context instructor "objectType" should fail numeric', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NUMERIC + ], + expect: [400] + }, + { + name: 'statement context instructor "objectType" should fail object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_OBJECT + ], + expect: [400] + }, + { + name: 'statement substatement as agent with "objectType" should fail numeric', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NUMERIC + ], + expect: [400] + }, + { + name: 'statement substatement as agent with "objectType" should fail object', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'}, + INVALID_OBJECTTYPE_OBJECT + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "objectType" should fail numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NUMERIC + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "objectType" should fail object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_OBJECT + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "objectType" should fail numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NUMERIC + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "objectType" should fail object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_OBJECT + ], + expect: [400] + } + ] + }, + { + /** XAPI-00033, Data 2.4.2.1 when the actor objectType is agent + * A "name" property is a String. If present, the LRS must validate and reject with 400 Bad Request if invalid. + */ + name: 'A "name" property is a String (Type, Data 2.4.2.1.s2.table1.row2, XAPI-00033)', + config: [ + { + name: 'statement actor "name" should fail numeric', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_NUMERIC + ], + expect: [400] + }, + { + name: 'statement actor "name" should fail object', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_OBJECT + ], + expect: [400] + }, + { + name: 'statement authority "name" should fail numeric', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_NUMERIC + ], + expect: [400] + }, + { + name: 'statement authority "name" should fail object', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_OBJECT + ], + expect: [400] + }, + { + name: 'statement context instructor "name" should fail numeric', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_NUMERIC + ], + expect: [400] + }, + { + name: 'statement context instructor "name" should fail object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_OBJECT + ], + expect: [400] + }, + { + name: 'statement substatement as agent with "name" should fail numeric', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_NUMERIC + ], + expect: [400] + }, + { + name: 'statement substatement as agent with "name" should fail object', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_OBJECT + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "name" should fail numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_NUMERIC + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "name" should fail object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_OBJECT + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "name" should fail numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_NUMERIC + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "name" should fail object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'}, + INVALID_OBJECTTYPE_NAME_OBJECT + ], + expect: [400] + } + ] + }, + { + /** XAPI-00034, Data 2.4.2.1 when the actor objectType is agent + * An "actor" property with "objectType" as "Agent" uses exactly one of the following Inverse Functional Identifier properties: "mbox", "mbox_sha1sum", "openid", "account". An LRS rejects with 400 Bad Request any agent object: + - Where the IFI property is absent + - Where the IFI value is invalid + - With more than one IFI + */ + name: 'An "actor" property with "objectType" as "Agent" uses one of the following properties: "mbox", "mbox_sha1sum", "openid", "account" (Multiplicity, Data 2.4.2.1.s2.b1, XAPI-00034)', + config: [ + { + name: 'statement actor without "account", "mbox", "mbox_sha1sum", "openid" should fail', + templates: [ + {statement: '{{statements.actor}}'} + ], + expect: [400] + }, + { + name: 'statement actor "account" should pass', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement actor "mbox" should pass', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox}}'} + ], + expect: [200] + }, + { + name: 'statement actor "mbox_sha1sum" should pass', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement actor "openid" should pass', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.openid}}'} + ], + expect: [200] + }, + { + name: 'statement authority without "account", "mbox", "mbox_sha1sum", "openid" should fail', + templates: [ + {statement: '{{statements.authority}}'} + ], + expect: [400] + }, + { + name: 'statement authority "account" should pass', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement authority "mbox" should pass', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox}}'} + ], + expect: [200] + }, + { + name: 'statement authority "mbox_sha1sum" should pass', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement authority "openid" should pass', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.openid}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor without "account", "mbox", "mbox_sha1sum", "openid" should fail', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'} + ], + expect: [400] + }, + { + name: 'statement context instructor "account" should pass', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor "mbox" should pass', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor "mbox_sha1sum" should pass', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor "openid" should pass', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as agent without "account", "mbox", "mbox_sha1sum", "openid" should fail', + templates: [ + {statement: '{{statements.object_actor}}'} + ], + expect: [400] + }, + { + name: 'statement substatement as agent "account" should pass', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as agent "mbox" should pass', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as agent "mbox_sha1sum" should pass', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as agent "openid" should pass', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.openid}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s agent without "account", "mbox", "mbox_sha1sum", "openid" should fail', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "account" should pass', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s agent "mbox" should pass', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.mbox}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s agent "mbox_sha1sum" should pass', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s agent "openid" should pass', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.openid}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor without "account", "mbox", "mbox_sha1sum", "openid" should fail', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "account" should pass', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor "mbox" should pass', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor "mbox_sha1sum" should pass', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor "openid" should pass', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'} + ], + expect: [200] + } + ] + }, + { //see above + name: 'An Agent is defined by "objectType" of an "actor" property or "object" property with value "Agent" (Data 2.4.2.1.s2.table1.row1, XAPI-00034)', + config: [ + { + name: 'statement actor does not require objectType', + templates: [ + {statement: '{{statements.no_actor}}'}, + { + "actor": { + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } + } + ], + expect: [200] + }, + { + name: 'statement actor "objectType" accepts "Agent"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'statement authority "objectType" accepts "Agent"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor "objectType" accepts "Agent"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as agent "objectType" accepts "Agent"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s agent "objectType" accepts "Agent"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor "objectType" accepts "Agent"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'} + ], + expect: [200] + }, + ] + }, + { //see above + name: 'An Agent does not use the "mbox" property if "mbox_sha1sum", "openid", or "account" are used (Multiplicity, Data 2.4.2.1.s2.b2, XAPI-00034)', + config: [ + { + name: 'statement actor "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement actor "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement actor "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement authority "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement authority "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement authority "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement as agent "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement as agent "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement as agent "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {actor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + ] + }, + { //see above + name: 'An Agent does not use the "mbox_sha1sum" property if "mbox", "openid", or "account" are used (Multiplicity, Data 2.4.2.1.s2.b2, XAPI-00034)', + config: [ + { + name: 'statement actor "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement actor "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement actor "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement authority "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement authority "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement authority "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement as agent "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement as agent "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement as agent "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {actor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + ] + }, + { //see above + name: 'An Agent does not use the "account" property if "mbox", "mbox_sha1sum", or "openid" are used (Multiplicity, Data 2.4.2.1.s2.b2, XAPI-00034)', + config: [ + { + name: 'statement actor "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement actor "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement actor "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement authority "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement authority "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement authority "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement context instructor "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement context instructor "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement context instructor "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement as agent "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement as agent "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement as agent "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {actor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + } + ] + }, + { //see above + name: 'An Agent does not use the "openid" property if "mbox", "mbox_sha1sum", or "account" are used (Multiplicity, Data 2.4.2.1.s2.b2, XAPI-00034)', + config: [ + { + name: 'statement actor "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement actor "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement actor "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement authority "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement authority "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement authority "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement context instructor "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement context instructor "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement context instructor "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement as agent "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement as agent "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement as agent "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {actor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/attachments.js b/test/v2_0/configs/attachments.js new file mode 100644 index 00000000..5bf22506 --- /dev/null +++ b/test/v2_0/configs/attachments.js @@ -0,0 +1,283 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_NUMERIC = 12345; + var INVALID_STRING = 'should fail'; + var INVALID_CONTENT_TYPE = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'contentType': INVALID_NUMERIC, + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }; + var INVALID_FILE_URL = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': INVALID_STRING + }; + var INVALID_LENGTH = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'contentType': 'text/plain; charset=ascii', + 'length': INVALID_STRING, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }; + var INVALID_SHA2 = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': INVALID_NUMERIC, + 'fileUrl': 'http://over.there.com/file.txt' + }; + var INVALID_USAGE_TYPE = { + 'usageType': INVALID_STRING, + 'display': {'en-US': 'A test attachment'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }; + var VALID_ATTACHMENT = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'description': {'en-US': 'A test attachment (description)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00025, Data 2.4 Statement Properties + * A Statement's "attachments" property is an array of Attachments. An LRS rejects with 400 Bad Request a statement which has an “attachments” property which is not an array of attachments. + */ + name: 'A Statement\'s "attachments" property is an array of Attachments (Data 2.4.s1.table1.row11, XAPI-00025)', + config: [ + { + name: 'statement "attachments" is an array', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [VALID_ATTACHMENT]} + ], + expect: [200] + }, + { + name: 'statement "attachments" not an array', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: VALID_ATTACHMENT} + ], + expect: [400] + } + ] + }, + { //this seems good - worth keeping + name: 'An Attachment is an Object (Definition, Data 2.4.11)', + config: [ + { + name: 'statement "attachment" invalid numeric', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [INVALID_NUMERIC]} + ], + expect: [400] + }, + { + name: 'statement "attachment" invalid string', + templates: [ + {statement: '{{statements.authority}}'}, + {attachments: [INVALID_STRING]} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00107, Data 2.4.11 Attachments + * A "usageType" property is an IRI. The LRS rejects with 400 Bad Request a statement which does not have a "usageType" property or the "usageType" property value is not a valid IRI in the Attachment Object. + */ + name: 'A "usageType" property is an IRI (Multiplicity, Data 2.4.11.s2.table1.row1, XAPI-00107)', + config: [ + { + name: 'statement "usageType" invalid string', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [INVALID_USAGE_TYPE]} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00105, Data 2.4.11 Attachment + * A "contentType" property is an Internet Media/MIME type. The LRS rejects with 400 Bad Request a statement which does not have a “contentType” property or the “contentType” property value is not Internet Media/MIME in the Attachment Object. + */ + name: 'A "contentType" property is an Internet Media/MIME type (Format, Data 2.4.11.s2.table1.row4, XAPI-00105)', + config: [ + { + name: 'statement "contentType" invalid number', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [INVALID_CONTENT_TYPE]}, + {attachments: {contentType: 999}} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00102, Data 2.4.11 Attachments + * A "length" property is an Integer. The LRS rejects with 400 Bad Request a statement whichdoes not have a “length” property or the “length” property is not a valid integer in octets in the Attachment Object. + */ + name: 'A "length" property is an Integer (Format, Data 2.4.11.s2.table1.row5, XAPI-00102)', + config: [ + { + name: 'statement "length" invalid string', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [INVALID_LENGTH]} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00103, Data 2.4.11 Attachments + * A "sha2" property is a String. The LRS rejects with 400 Bad Request a statement which does not have a “sha2” property or the ”sha2” property is not a valid hash in the Attachment Object. + */ + name: 'A "sha2" property is a String (Format, Data 2.4.11.s2.table1.row6, XAPI-00103)', + config: [ + { + name: 'statement "sha2" invalid string', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [INVALID_SHA2]} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00104, Data 2.4.11 Attachments + * A "fileUrl" property is an IRL. The LRS rejects with 400 Bad Request a statement the “fileURL” property if it is present and it is not a valid IRL in the Attachment Object. + */ + name: 'A "fileUrl" property is an IRL (Format, Data 2.4.11.s2.table1.row7, XAPI-00104)', + config: [ + { + name: 'statement "fileUrl" invalid string', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [INVALID_FILE_URL]} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00106, Data 2.4.11 Attachments + * A "display" property is a Language Map. The LRS rejects with 400 Bad Request a statement which does not have a “display” property or the “display” property value is not a valid Language Map in the Attachment Object. + */ + name: 'A "display" property is a Language Map (Type, Data 2.4.11.s2.table1.row2, XAPI-00106)', + config: [ + { + name: 'statement attachment "display" language map numeric', + templates: [ + {statement: '{{statements.attachment}}'}, + { + attachments: [ + { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': INVALID_NUMERIC, + 'description': {'en-US': 'A test attachment (description)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + } + ] + } + ], + expect: [400] + }, + { + name: 'statement attachment "display" language map string', + templates: [ + {statement: '{{statements.attachment}}'}, + { + attachments: [ + { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': INVALID_STRING, + 'description': {'en-US': 'A test attachment (description)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + } + ] + } + ], + expect: [400] + }, + { + name: 'statement attachment "description" language map numeric', + templates: [ + {statement: '{{statements.attachment}}'}, + { + attachments: [ + { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'description': INVALID_NUMERIC, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + } + ] + } + ], + expect: [400] + }, + { + name: 'statement attachment "description" language map string', + templates: [ + {statement: '{{statements.attachment}}'}, + { + attachments: [ + { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'description': 'should error', + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + } + ] + } + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/authorities.js b/test/v2_0/configs/authorities.js new file mode 100644 index 00000000..444a5111 --- /dev/null +++ b/test/v2_0/configs/authorities.js @@ -0,0 +1,171 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_ONE_MEMBER = [ + { + "mbox": "mailto:bob@example.com" + } + ]; + var INVALID_THREE_MEMBER = [ + { + "account": { + "homePage": "http://example.com/xAPI/OAuth/Token", + "name": "oauth_consumer_x75db" + } + }, + { + "mbox": "mailto:bob@example.com" + }, + { + "mbox": "mailto:james@example.com" + } + ]; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00024, Data 2.4 Statement Properties + * An "authority" property is an Agent or Group. An LRS rejects with 400 Bad Request a statement which has an “authority” property which is not Agent or Group. + */ + name: 'An "authority" property is an Agent or Group (Type, Data 2.4.s1.table1.row9, XAPI-00024)', + config: [ + { + name: 'should pass statement authority agent template', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement authority template', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_two_member}}'} + ], + expect: [200] + }, + { + name: 'should fail statement authority identified group (mbox)', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_two_member}}'}, + {"mbox": "mailto:bob@example.com"} + ], + expect: [400] + }, + { + name: 'should fail statement authority identified group (mbox_sha1sum)', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_two_member}}'}, + {"mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975"} + ], + expect: [400] + }, + { + name: 'should fail statement authority identified group (openid)', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_two_member}}'}, + {"openid": "http://openid.example.org/12345"} + ], + expect: [400] + }, + { + name: 'should fail statement authority identified group (account)', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_two_member}}'}, + { + "account": { + "homePage": "http://www.example.com", + "name": "xAPI account name" + } + } + ], + expect: [400] + } + ] + }, + { + /** XAPI-00098, Data 2.4.9 Authority + * An "authority" property which is also a Group contains exactly two Agents. The LRS rejects with 400 Bad Request a statement which has an “authority” property with a “objectType” of “Group” with more or less than two Oauth Agents as values of the “member” property. + */ + name: 'An "authority" property which is also a Group contains exactly two Agents (Type, Data 2.4.9.s3.b1, XAPI-00098)', + config: [ + { + name: 'statement "authority" invalid one member', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_no_member}}'}, + {member: INVALID_ONE_MEMBER} + ], + expect: [400] + }, + { + name: 'statement "authority" invalid three member', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_no_member}}'}, + {member: INVALID_THREE_MEMBER} + ], + expect: [400] + } + ] + }, + { //see above + name: 'Statement authority shall only be an anonymous group with two members (Data 2.4.9.s3.b1, XAPI-00098)', + config: [ + { + name: 'statement authority identified group is rejected', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_openid}}'} + ], + expect: [400] + }, + { + name: 'statement authority anonymous group with two members is accepted', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_two_member}}'} + ], + expect: [200] + }, + { + name: 'statement authority anonymous group without two members is rejected', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_no_member}}'} + ], + expect: [400] + } + ] + }, + { //see above + name: 'An LRS rejects with error code 400 Bad Request, a Request whose "authority" is a Group of more than two Agents (Format, Data 2.4.9.s3.b1, XAPI-00098)', + config: [ + { + name: 'statement "authority" invalid three member', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_no_member}}'}, + {member: INVALID_THREE_MEMBER} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/contextactivities.js b/test/v2_0/configs/contextactivities.js new file mode 100644 index 00000000..b10620dc --- /dev/null +++ b/test/v2_0/configs/contextactivities.js @@ -0,0 +1,276 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // Defines overwriting data. + var INVALID_OBJECT = {key: 'should fail'}; + var VALID_ACTIVITY = { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + }; + + // Configures tests. + module.exports.config = function () { + return [ + { + /** XAPI-00093, Data 2.4.6.2 ContextActivities Property + * A "contextActivities" property's "key" has a value of "parent", "grouping", "category", or "other". The LRS rejects with 400 Bad Request a statement which has a key other than "parent", "grouping", "category", or "other" for the “contextActivities” property + */ + name: 'A "contextActivities" property\'s "key" has a value of "parent", "grouping", "category", or "other" (Format, Data 2.4.6.2.s4.b1, XAPI-00093)', + config: [ + { + name: 'statement context "contextActivities" is "parent"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.parent}}'} + ], + expect: [200] + }, + { + name: 'statement context "contextActivities" is "grouping"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.grouping}}'} + ], + expect: [200] + }, + { + name: 'statement context "contextActivities" is "category"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.category}}'} + ], + expect: [200] + }, + { + name: 'statement context "contextActivities" is "other"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.other}}'} + ], + expect: [200] + }, + { + name: 'statement context "contextActivities" accepts all property keys "parent", "grouping", "category", and "other"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.all_activities}}'} + ], + expect: [200] + }, + { + name: 'statement context "contextActivities" rejects any property key other than "parent", "grouping", "category", or "other"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.invalid_activity}}'} + ], + expect: [400] + }, + { + name: 'statement substatement context "contextActivities" is "parent"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.parent}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities" is "grouping"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.grouping}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities" is "category"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.category}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities" is "other"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.other}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities" accepts all property keys "parent", "grouping", "category", and "other"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.all_activities}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities" rejects any property key other than "parent", "grouping", "category", or "other"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.invalid_activity}}'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00094, Data 2.4.6.2 ContextActivities Property + * A "contextActivities" property's "value" is an Activity. The LRS rejects with 400 Bad Request a statement which has a value which is not an Activity for the “contextActivities” property. + */ + name: 'A "contextActivities" property\'s "value" is an Activity (Format, Data 2.4.6.2.s4.b2, XAPI-00094)', + config: [ + { + name: 'statement context "contextActivities parent" value is activity array', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.parent}}'}, + { + contextActivities: { + parent: [VALID_ACTIVITY] + } + } + ], + expect: [200] + }, + { + name: 'statement context "contextActivities grouping" value is activity array', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.grouping}}'}, + { + contextActivities: { + grouping: [VALID_ACTIVITY] + } + } + ], + expect: [200] + }, + { + name: 'statement context "contextActivities category" value is activity array', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.category}}'}, + { + contextActivities: { + category: [VALID_ACTIVITY] + } + } + ], + expect: [200] + }, + { + name: 'statement context "contextActivities other" value is activity array', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.other}}'}, + { + contextActivities: { + other: [VALID_ACTIVITY] + } + } + ], + expect: [200] + }, + { + name: 'statement context contextActivities property\'s value is activity array with activities', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.other}}'}, + { + contextActivities: { + other: [VALID_ACTIVITY, INVALID_OBJECT] + } + } + ], + expect: [400] + }, + { + name: 'statement substatement context "contextActivities parent" value is activity array', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.parent}}'}, + { + contextActivities: { + parent: [VALID_ACTIVITY] + } + } + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities grouping" value is activity array', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.grouping}}'}, + { + contextActivities: { + grouping: [VALID_ACTIVITY] + } + } + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities category" value is activity array', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.category}}'}, + { + contextActivities: { + category: [VALID_ACTIVITY] + } + } + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities other" value is activity array', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.other}}'}, + { + contextActivities: { + other: [VALID_ACTIVITY] + } + } + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities property\'s value is activity array', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.other}}'}, + { + contextActivities: { + other: [INVALID_OBJECT, VALID_ACTIVITY] + } + } + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/contextagents.js b/test/v2_0/configs/contextagents.js new file mode 100644 index 00000000..b728b4b3 --- /dev/null +++ b/test/v2_0/configs/contextagents.js @@ -0,0 +1,168 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // Defines overwriting data. + var CONTEXT_WHOSE_contextAgents_HAS_INVALID_OBJECT_TYPE = { + contextAgents: [ + { + objectType: "Should fail", + agent: { + objectType: "Agent", + mbox: "mailto:player-1@example.com" + } + } + ] + }; + var CONTEXT_WHOSE_contextAgents_HAS_INVALID_AGENT = { + contextAgents: [ + { + objectType: "contextAgent", + agent: { + key: "Should fail" + } + } + ] + }; + + var INVALID_RELEVANT_TYPE_IS_EMPTY = { + contextAgents: [ + { + objectType: "contextAgent", + agent: { + + objectType: "Agent", + mbox: "mailto:player-1@example.com" + }, + relevantTypes: [] + } + ] + }; + + var INVALID_RELEVANT_TYPE_NON_IRI_ELEMENT = { + contextAgents: [ + { + objectType: "contextAgent", + agent: { + objectType: "Agent", + mbox: "mailto:player-1@example.com" + }, + relevantTypes: ['abc'] + } + ] + }; + + // Configures tests. + module.exports.config = function () { + return [ + { + /** ContextAgents Property + * A "contextAgents" property is an array of "contextAgent" Objects. + */ + name: 'A "contextAgents" property is an array of "contextAgent" Objects.', + config: [ + { + name: 'Statement with "contextAgents" property has an array of valid "contextAgent" Objects', + templates: [ + { statement: '{{statements.context}}' }, + { context: '{{contexts.context_agents}}' } + ], + expect: [200] + }, + { + name: 'Statement substatement with "contextAgents" property has an array of valid "contextAgent" Objects', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: '{{contexts.context_agents}}' } + ], + expect: [200] + } + ] + }, + { + /** ContextAgents Object + * A "contextAgents" Object must have an "objectType" property of string "contextAgent" and a valid Agent Object "agent"; + * it may also have a "relevantTypes" property as an array of Activity Type IRIs. + */ + name: 'A "contextAgents" Object must have an "objectType" property of string "contextAgent" and a valid Agent Object "agent"', + config: [ + { + name: 'Statement with "contextAgents" Object rejects statement if "objectType" property is anything other than string "contextAgent"', + templates: [ + { statement: '{{statements.context}}' }, + { context: CONTEXT_WHOSE_contextAgents_HAS_INVALID_OBJECT_TYPE } + ], + expect: [400] + }, + { + name: 'Statement with "contextAgents" Object rejects statement if "agent" property is invalid', + templates: [ + { statement: '{{statements.context}}' }, + { context: CONTEXT_WHOSE_contextAgents_HAS_INVALID_AGENT } + ], + expect: [400] + }, + { + name: 'Statement with "contextAgents" Object rejects statement if "relevantTypes" is empty', + templates: [ + { statement: '{{statements.context}}' }, + { context: INVALID_RELEVANT_TYPE_IS_EMPTY } + ], + expect: [400] + }, + { + name: 'Statement with "contextAgents" Object rejects statement if "relevantTypes" contains non-IRI elements', + templates: [ + { statement: '{{statements.context}}' }, + { context: INVALID_RELEVANT_TYPE_NON_IRI_ELEMENT} + ], + expect: [400] + }, + { + name: 'Statement substatement with "contextAgents" Object rejects statement if "objectType" property is anything other than string "contextAgent"', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: CONTEXT_WHOSE_contextAgents_HAS_INVALID_OBJECT_TYPE } + ], + expect: [400] + }, + { + name: 'Statement substatement with "contextAgents" Object rejects statement if "agent" property is invalid', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: CONTEXT_WHOSE_contextAgents_HAS_INVALID_AGENT } + ], + expect: [400] + }, + { + name: 'Statement substatement with "contextAgents" Object rejects statement if "relevantTypes" is empty', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: INVALID_RELEVANT_TYPE_IS_EMPTY } + ], + expect: [400] + }, + { + name: 'Statement substatement with "contextAgents" Object rejects statement if "relevantTypes" contains non-IRI elements', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: INVALID_RELEVANT_TYPE_NON_IRI_ELEMENT} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/contextgroups.js b/test/v2_0/configs/contextgroups.js new file mode 100644 index 00000000..436a3cfb --- /dev/null +++ b/test/v2_0/configs/contextgroups.js @@ -0,0 +1,174 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // Defines overwriting data. + var CONTEXT_WHOSE_contextGroups_HAS_INVALID_OBJECT_TYPE = { + contextGroups: [ + { + objectType: "Should fail", + group: { + objectType: "Group", + mbox: "mailto:team-1@example.com" + }, + member: [ + { + objectType: "Agent", + mbox: "mailto:player-1@example.com" + } + ], + } + ] + }; + var CONTEXT_WHOSE_contextGroups_HAS_INVALID_GROUP = { + contextGroups: [ + { + objectType: "contextGroup", + group: { + key: "Should fail" + } + } + ] + }; + + var INVALID_RELEVANT_TYPE_IS_EMPTY = { + contextGroups: [ + { + objectType: "contextGroup", + group: { + + objectType: "Group", + mbox: "mailto:player-1@example.com" + }, + relevantTypes: [] + } + ] + }; + + var INVALID_RELEVANT_TYPE_NON_IRI_ELEMENT = { + contextGroups: [ + { + objectType: "contextGroup", + group: { + objectType: "Group", + mbox: "mailto:player-1@example.com" + }, + relevantTypes: ['abc'] + } + ] + }; + + // Configures tests. + module.exports.config = function () { + return [ + { + /** ContextGroups Property + * A "contextGroups" property is an array of "contextGroup" Objects. + */ + name: 'A "contextGroups" property is an array of "contextGroup" Objects', + config: [ + { + name: 'Statement with "contextGroups" property has an array of valid "contextGroup" Objects', + templates: [ + { statement: '{{statements.context}}' }, + { context: '{{contexts.context_groups}}' } + ], + expect: [200] + }, + { + name: 'Statement substatement with "contextGroups" property has an array of valid "contextGroup" Objects', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: '{{contexts.context_groups}}' } + ], + expect: [200] + } + ] + }, + { + /** ContextGroups Object + * A "contextGroups" Object must have an "objectType" property of string "contextGroup" and a valid Group Object "group"; + * it may also have a "relevantTypes" property as an array of Activity Type IRIs. + */ + name: 'A "contextGroups" Object must have an "objectType" property of string "contextGroup" and a valid Group Object "group"', + config: [ + { + name: 'Statement with "contextGroups" Object rejects statement if "objectType" property is anything other than string "contextGroup"', + templates: [ + { statement: '{{statements.context}}' }, + { context: CONTEXT_WHOSE_contextGroups_HAS_INVALID_OBJECT_TYPE } + ], + expect: [400] + }, + { + name: 'Statement with "contextGroups" Object rejects statement if "group" property is invalid', + templates: [ + { statement: '{{statements.context}}' }, + { context: CONTEXT_WHOSE_contextGroups_HAS_INVALID_GROUP } + ], + expect: [400] + }, + { + name: 'Statement with "contextGroups" Object rejects statement if "relevantTypes" is empty', + templates: [ + { statement: '{{statements.context}}' }, + { context: INVALID_RELEVANT_TYPE_IS_EMPTY } + ], + expect: [400] + }, + { + name: 'Statement with "contextGroups" Object rejects statement if "relevantTypes" contains non-IRI elements', + templates: [ + { statement: '{{statements.context}}' }, + { context: INVALID_RELEVANT_TYPE_NON_IRI_ELEMENT} + ], + expect: [400] + }, + { + name: 'Statement substatement with "contextGroups" Object rejects statement if "objectType" property is anything other than string "contextGroup"', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: CONTEXT_WHOSE_contextGroups_HAS_INVALID_OBJECT_TYPE } + ], + expect: [400] + }, + { + name: 'Statement substatement with "contextGroups" Object rejects statement if "group" property is invalid', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: CONTEXT_WHOSE_contextGroups_HAS_INVALID_GROUP } + ], + expect: [400] + }, + { + name: 'Statement substatement with "contextGroups" Object rejects statement if "relevantTypes" is empty', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: INVALID_RELEVANT_TYPE_IS_EMPTY } + ], + expect: [400] + }, + { + name: 'Statement substatement with "contextGroups" Object rejects statement if "relevantTypes" contains non-IRI elements', + templates: [ + { statement: '{{statements.object_substatement}}' }, + { object: '{{substatements.context}}' }, + { context: INVALID_RELEVANT_TYPE_NON_IRI_ELEMENT} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/contexts.js b/test/v2_0/configs/contexts.js new file mode 100644 index 00000000..893921df --- /dev/null +++ b/test/v2_0/configs/contexts.js @@ -0,0 +1,582 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_LANGUAGE = {a12345: 'should fail'}; + var INVALID_NUMERIC = 12345; + var INVALID_OBJECT = {key: 'should fail'}; + var INVALID_STRING = 'should fail'; + var VALID_ACTIVITY = { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + }; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00087-1, Data 2.4.6 Context + * A "registration" property is a UUID. The LRS rejects with 400 Bad Request a statement with a “registration” property which is not a valid UUID. + */ + name: 'A "registration" property is a UUID (Type, Data 2.4.6.s3.table1.row1, XAPI-00087-1)', + config: [ + { + name: 'statement context "registration" is object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {registration: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement context "registration" is string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {registration: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement substatement context "registration" is object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {registration: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement context "registration" is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {registration: INVALID_STRING} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00087-2, Data 2.4.6 Context + * An "instructor" property is an Agent. The LRS rejects with 400 Bad Request a statement with an “instructor” property which is not a valid Agent. + */ + name: 'An "instructor" property is an Agent (Type, Data 2.4.6.s3.table1.row2, XAPI-00087-2)', + config: [ + { + name: 'statement context "instructor" is object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement context "instructor" is string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement substatement context "instructor" is object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement context "instructor" is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: INVALID_STRING} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00088, Data 2.4.6 Context + * A "team" property is a Group. The LRS rejects with 400 Bad Request a statement with a “team” property which is not a valid Group. + */ + name: 'An "team" property is a Group (Type, Data 2.4.6.s3.table1.row3, XAPI-00088)', + config: [ + { + name: 'statement context "team" is agent', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'statement context "team" is object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement context "team" is string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement substatement context "team" is agent', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'statement substatement context "team" is object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement context "team" is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: INVALID_STRING} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00086, Data 2.4.6 Context + * A "contextActivities" property is an Object. The LRS rejects with 400 Bad Request a statement with a “contextActivities” property which is not a valid object. + */ + name: 'A "contextActivities" property is an Object (Type, Data 2.4.6.s3.table1.row4, XAPI-00086)', + config: [ + { + name: 'statement context "contextActivities" is string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {contextActivities: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement substatement context "contextActivities" is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {contextActivities: INVALID_STRING} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00089, Data 2.4.6 Context + * A "revision" property is a String. The LRS rejects with 400 Bad Request a statement with a “revision” property which is not a valid string. + */ + name: 'A "revision" property is a String (Type, Data 2.4.6.s3.table1.row5, XAPI-00089)', + config: [ + { + name: 'statement context "revision" is numeric', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {revision: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement context "revision" is object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {revision: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement context "revision" is numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {revision: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement substatement context "revision" is object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {revision: INVALID_OBJECT} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00084, Data 2.4.6 Context + * A Statement cannot contain both a "revision" property in its "context" property and have the value of the "object" property's "objectType" be anything but "Activity". The LRS rejects a statement with 400 Bad Request if contains a "revision" property in its "context" property and does not have an Object with an “objectType” value of “activity” or an Object where the “objectType” property is absent. + */ + name: 'A Statement cannot contain both a "revision" property in its "context" property and have the value of the "object" property\'s "objectType" be anything but "Activity" (Data 2.4.6.s4.b1, XAPI-00084)', + config: [ + { + name: 'statement context "revision" is invalid with object agent', + templates: [ + {statement: '{{statements.object_agent_default}}'}, + {context: '{{contexts.no_platform}}'} + ], + expect: [400] + }, + { + name: 'statement context "revision" is invalid with object group', + templates: [ + {statement: '{{statements.object_group_default}}'}, + {context: '{{contexts.no_platform}}'} + ], + expect: [400] + }, + { + name: 'statement context "revision" is invalid with statementref', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {context: '{{contexts.no_platform}}'} + ], + expect: [400] + }, + { + name: 'statement context "revision" is invalid with substatement', + templates: [ + {statement: '{{statements.object_substatement_default}}'}, + {context: '{{contexts.no_platform}}'} + ], + expect: [400] + }, + { + name: 'statement context "revision" is valid with no ObjectType', + templates: [ + {statement: '{{statements.context_sans_objectType}}'}, + {context: '{{contexts.no_platform}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "revision" is invalid with object agent', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.agent_default}}'}, + {context: '{{contexts.no_platform}}'} + ], + expect: [400] + }, + { + name: 'statement substatement context "revision" is invalid with object group', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.group_default}}'}, + {context: '{{contexts.no_platform}}'} + ], + expect: [400] + }, + { + name: 'statement substatement context "revision" is invalid with statementref', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'}, + {context: '{{contexts.no_platform}}'} + ], + expect: [400] + }, + { + name: 'statement substatement context "revision" is valid with no objectType', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{statements.context_sans_objectType}}'}, + {context: '{{contexts.no_platform}}'} + ], + expect: [200] + } + ] + }, + { + /** XAPI-00090, Data 2.4.6 Context + * A "platform" property is a String. The LRS rejects with 400 Bad Request a statement with a “platform” property which is not a valid string. + */ + name: 'A "platform" property is a String (Type, Data 2.4.6.s3.table1.row6, XAPI-00090)', + config: [ + { + name: 'statement context "platform" is numeric', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {platform: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement context "platform" is object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {platform: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement context "platform" is numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {platform: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement substatement context "platform" is object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {platform: INVALID_OBJECT} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00085, Data 2.4.6 Context + * A Statement cannot contain a "platform" property in its "context" property and have the value of the "object" property's "objectType" be anything but "Activity". The LRS rejects a statement with 400 Bad Request if contains a "platform" property in its "context" property and does not have an Object with an “objectType” value of “activity” or an Object where the “objectType” property is absent. + */ + name: 'A Statement cannot contain both a "platform" property in its "context" property and have the value of the "object" property\'s "objectType" be anything but "Activity" (Data 2.4.6.s4.b2, XAPI-00085)', + config: [ + { + name: 'statement context "platform" is invalid with object agent', + templates: [ + {statement: '{{statements.object_agent_default}}'}, + {context: '{{contexts.no_revision}}'} + ], + expect: [400] + }, + { + name: 'statement context "platform" is invalid with object group', + templates: [ + {statement: '{{statements.object_group_default}}'}, + {context: '{{contexts.no_revision}}'} + ], + expect: [400] + }, + { + name: 'statement context "platform" is invalid with statementref', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {context: '{{contexts.no_revision}}'} + ], + expect: [400] + }, + { + name: 'statement context "platform" is invalid with substatement', + templates: [ + {statement: '{{statements.object_substatement_default}}'}, + {context: '{{contexts.no_revision}}'} + ], + expect: [400] + }, + { + name: 'statement context "platform" is valid with empty objectType', + templates: [ + {statement: '{{statements.context_sans_objectType}}'}, + {context: '{{contexts.no_revision}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "platform" is invalid with object agent', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.agent_default}}'}, + {context: '{{contexts.no_revision}}'} + ], + expect: [400] + }, + { + name: 'statement substatement context "platform" is invalid with object group', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.group_default}}'}, + {context: '{{contexts.no_revision}}'} + ], + expect: [400] + }, + { + name: 'statement substatement context "platform" is invalid with statementref', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'}, + {context: '{{contexts.no_revision}}'} + ], + expect: [400] + }, + { + name: 'statement substatement context "platform" is valid with empty ObjectType', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{statements.context_sans_objectType}}'}, + {context: '{{contexts.no_revision}}'} + ], + expect: [200] + } + ] + }, + { + /** XAPI-00091, Data 2.4.6 Context + * A "language" property is a String which follows RFC 5646. The LRS rejects with 400 Bad Request a statement with a “language” property which is not a valid RFC 5646 string. + * And see below + */ + name: 'A "language" property is a String (Type, Data 2.4.6.s3.table1.row7, XAPI-00091)', + config: [ + { + name: 'statement context "language" is numeric', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {language: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement context "language" is object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {language: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement context "language" is numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {language: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement substatement context "language" is object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {language: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement context "language" is is invalid language', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {language: INVALID_LANGUAGE} + ], + expect: [400] + }, + { + name: 'statement substatement context "language" is is invalid language', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {language: INVALID_LANGUAGE} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00092, Data 2.4.6 Context + * A "statement" property is a Statement Reference. The LRS rejects with 400 Bad Request a statement with a “statement” property which is not a valid StatementRef. + */ + name: 'A "statement" property is a Statement Reference (Type, Data 2.4.6.s3.table1.row8, XAPI-00092)', + config: [ + { + name: 'statement context "statement" invalid with "statementref"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {statement: {objectType: 'statementref'}} + ], + expect: [400] + }, + { + name: 'statement context "statement" invalid with "id" not UUID', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {statement: {id: INVALID_STRING}} + ], + expect: [400] + }, + { + name: 'statement substatement context "statement" invalid with "statementref"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {statement: {objectType: 'statementref'}} + ], + expect: [400] + }, + { + name: 'statement substatement context "statement" invalid with "id" not UUID', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + {statement: {id: INVALID_STRING}} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/durations.js b/test/v2_0/configs/durations.js new file mode 100644 index 00000000..1ecbc953 --- /dev/null +++ b/test/v2_0/configs/durations.js @@ -0,0 +1,186 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_DURATION = 'PA1H0M0S'; + var INVALID_NUMERIC = 12345; + var INVALID_OBJECT = {key: 'invalid'}; + var INVALID_STRING = 'should fail'; + var VALID_DURATION = 'PT1H0M0.1S'; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00124, Data 2.4.5 result + * A Duration MUST be expressed using the format for Duration in ISO 8601:2004(E) section 4.4.3.2. + * The alternative format (in conformity with the format used for time points and described in ISO 8601:2004(E) section 4.4.3.3) + * MUST NOT be used. + * The LRS rejects with 400 a statement which includes the “duration” property and the value does not validate to + * ISO 8601:2004(E) section 4.4.3.2. + */ + name: 'A Duration MUST be expressed using the format for Duration in ISO 8601:2004(E) section 4.4.3.2. (Type, Data 4.6.s1.b1, XAPI-00124)', + config: [ + { + name: 'Statement result "duration" property is valid', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: VALID_DURATION} + ], + expect: [200] + }, + { + name: 'Statement substatement result "duration" property is valid', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: VALID_DURATION} + ], + expect: [200] + }, + { + name: 'statement result "duration" property is invalid with invalid string', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement substatement result "duration" property is invalid', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement result "duration" property is invalid with invalid number', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement substatement result "duration" property is invalid invalid number', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement result "duration" property is invalid with invalid object', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement result "duration" property is invalid with invalid object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement result "duration" property is invalid with invalid duration', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_DURATION} + ], + expect: [400] + }, + { + name: 'statement substatement result "duration" property is invalid with invalid duration', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_DURATION} + ], + expect: [400] + }, + { + name: 'statement result "duration" property is valid with "PT4H35M59.14S"', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'PT4H35M59.14S'} + ], + expect: [200] + }, + { + name: 'statement substatement result "duration" property is valid with "PT16559.14S"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'PT16559.14S'} + ], + expect: [200] + }, + { + name: 'statement result "duration" property is valid with "P3Y1M29DT4H35M59.14S"', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'P3Y1M29DT4H35M59.14S'} + ], + expect: [200] + }, + { + name: 'statement substatement result "duration" property is valid with "P3Y"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'P3Y'} + ], + expect: [200] + }, + { + name: 'statement result "duration" property is valid with "P4W"', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'P4W'} + ], + expect: [200] + }, + { + name: 'statement result "duration" property is invalid with "P4W1D"', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'P4W1D'} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); \ No newline at end of file diff --git a/test/v2_0/configs/extensions.js b/test/v2_0/configs/extensions.js new file mode 100644 index 00000000..5acdbbda --- /dev/null +++ b/test/v2_0/configs/extensions.js @@ -0,0 +1,566 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_EXTENSION_KEY = {'extensions': {'should fail': true}}; + var NULL_VALUE = {'extensions': {'http://example.com/ex': null}}; + var EMPTY_STRING_VALUE = {'extensions': {'http://example.com/ex': ''}}; + var EMPTY_OBJECT_VALUE = {'extensions': {'http://example.com/ex': {}}}; + var VALID_EXTENSION_EMPTY = {'extensions': {}}; + var VALID_EXTENSION_BOOLEAN = {'extensions': {'http://example.com/ex': true}}; + var VALID_EXTENSION_NUMERIC = {'extensions': {'http://example.com/ex': 12345}}; + var VALID_EXTENSION_OBJECT = {'extensions': {'http://example.com/ex': {key: 'valid'}}}; + var VALID_EXTENSION_STRING = {'extensions': {'http://example.com/ex': 'valid'}}; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00120, Data 4.1 Extensions + * An Extension's structure is that of "key"/"value" pairs. The LRS rejects with 400 a statement which does not use valid “key”/”value” pairs in the Extension property + * See XAPI-00118 for the 400 + */ + name: 'An Extension is defined as an Object of any "extensions" property (Multiplicity, Data 4.1.s2, XAPI-00120)', + config: [ + { + name: 'statement activity extensions valid boolean', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'}, + {definition: VALID_EXTENSION_BOOLEAN} + ], + expect: [200] + }, + { + name: 'statement activity extensions valid numeric', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'}, + {definition: VALID_EXTENSION_NUMERIC} + ], + expect: [200] + }, + { + name: 'statement activity extensions valid object', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'}, + {definition: VALID_EXTENSION_OBJECT} + ], + expect: [200] + }, + { + name: 'statement activity extensions valid string', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'}, + {definition: VALID_EXTENSION_STRING} + ], + expect: [200] + }, + { + name: 'statement result extensions valid boolean', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + VALID_EXTENSION_BOOLEAN + ], + expect: [200] + }, + { + name: 'statement result extensions valid numeric', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + VALID_EXTENSION_NUMERIC + ], + expect: [200] + }, + { + name: 'statement result extensions valid object', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + VALID_EXTENSION_OBJECT + ], + expect: [200] + }, + { + name: 'statement result extensions valid string', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + VALID_EXTENSION_STRING + ], + expect: [200] + }, + { + name: 'statement context extensions valid boolean', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + VALID_EXTENSION_BOOLEAN + ], + expect: [200] + }, + { + name: 'statement context extensions valid numeric', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + VALID_EXTENSION_NUMERIC + ], + expect: [200] + }, + { + name: 'statement context extensions valid object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + VALID_EXTENSION_OBJECT + ], + expect: [200] + }, + { + name: 'statement context extensions valid string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + VALID_EXTENSION_STRING + ], + expect: [200] + }, + { + name: 'statement substatement activity extensions valid boolean', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'}, + {definition: VALID_EXTENSION_BOOLEAN} + ], + expect: [200] + }, + { + name: 'statement substatement activity extensions valid numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'}, + {definition: VALID_EXTENSION_NUMERIC} + ], + expect: [200] + }, + { + name: 'statement substatement activity extensions valid object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'}, + {definition: VALID_EXTENSION_OBJECT} + ], + expect: [200] + }, + { + name: 'statement substatement activity extensions valid string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'}, + {definition: VALID_EXTENSION_STRING} + ], + expect: [200] + }, + { + name: 'statement substatement result extensions valid boolean', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + VALID_EXTENSION_BOOLEAN + ], + expect: [200] + }, + { + name: 'statement substatement result extensions valid numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + VALID_EXTENSION_NUMERIC + ], + expect: [200] + }, + { + name: 'statement substatement result extensions valid object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + VALID_EXTENSION_OBJECT + ], + expect: [200] + }, + { + name: 'statement substatement result extensions valid string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + VALID_EXTENSION_STRING + ], + expect: [200] + }, + { + name: 'statement substatement context extensions valid boolean', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + VALID_EXTENSION_BOOLEAN + ], + expect: [200] + }, + { + name: 'statement substatement context extensions valid numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + VALID_EXTENSION_NUMERIC + ], + expect: [200] + }, + { + name: 'statement substatement context extensions valid object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + VALID_EXTENSION_OBJECT + ], + expect: [200] + }, + { + name: 'statement substatement context extensions valid string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + VALID_EXTENSION_STRING + ], + expect: [200] + } + ] + }, + { + /** XAPI-00119, Data 4.1 Extensions + * An Extension can be null, an empty string, objects with nothing in them. The LRS accepts with 200 if a PUT or 204 if a POST an otherwise valid statement which has any extension value including null, an empty string, or an empty object. + * These are all emptys and POST + */ + name: 'An Extension can be null, an empty string, objects with nothing in them when using POST. (Format, Data 4.1, XAPI-00119)', + config: [ + { + name: 'statement activity extensions can be empty object', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: VALID_EXTENSION_EMPTY} + ], + expect: [200] + }, + { + name: 'statement activity extension values can be empty string', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: EMPTY_STRING_VALUE} + ], + expect: [200] + }, + { + name: 'statement activity extension values can be null', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: NULL_VALUE} + ], + expect: [200] + }, + { + name: 'statement activity extension values can be empty object', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: EMPTY_OBJECT_VALUE} + ], + expect: [200] + }, + { + name: 'statement result extensions can be empty object', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.no_extensions}}'}, + VALID_EXTENSION_EMPTY + ], + expect: [200] + }, + { + name: 'statement result extension values can be empty string', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.no_extensions}}'}, + EMPTY_STRING_VALUE + ], + expect: [200] + }, + { + name: 'statement result extension values can be null', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.no_extensions}}'}, + NULL_VALUE + ], + expect: [200] + }, + { + name: 'statement result extension values can be empty object', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.no_extensions}}'}, + EMPTY_OBJECT_VALUE + ], + expect: [200] + }, + { + name: 'statement context extensions can be empty object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + VALID_EXTENSION_EMPTY + ], + expect: [200] + }, + { + name: 'statement context extensions can be empty string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + EMPTY_STRING_VALUE + ], + expect: [200] + }, + { + name: 'statement context extensions can be null', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + NULL_VALUE + ], + expect: [200] + }, + { + name: 'statement context extensions can be empty object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + EMPTY_OBJECT_VALUE + ], + expect: [200] + }, + { + name: 'statement substatement activity extensions can be empty object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: VALID_EXTENSION_EMPTY} + ], + expect: [200] + }, + { + name: 'statement substatement activity extension values can be empty string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: EMPTY_STRING_VALUE} + ], + expect: [200] + }, + { + name: 'statement substatement activity extension values can be null', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: NULL_VALUE} + ], + expect: [200] + }, + { + name: 'statement substatement activity extension values can be empty object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: EMPTY_OBJECT_VALUE} + ], + expect: [200] + }, + { + name: 'statement substatement result extensions can be empty object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.no_extensions}}'}, + VALID_EXTENSION_EMPTY + ], + expect: [200] + }, + { + name: 'statement substatement result extensions can be empty string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.no_extensions}}'}, + EMPTY_STRING_VALUE + ], + expect: [200] + }, + { + name: 'statement substatement result extensions can be null', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.no_extensions}}'}, + NULL_VALUE + ], + expect: [200] + }, + { + name: 'statement substatement result extensions can be empty object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.no_extensions}}'}, + EMPTY_OBJECT_VALUE + ], + expect: [200] + }, + { + name: 'statement substatement context extensions can be empty object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + VALID_EXTENSION_EMPTY + ], + expect: [200] + }, + { + name: 'statement substatement context extensions can be empty string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + EMPTY_STRING_VALUE + ], + expect: [200] + }, + { + name: 'statement substatement context extensions can be null', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + NULL_VALUE + ], + expect: [200] + }, + { + name: 'statement substatement context extensions can be empty object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + EMPTY_OBJECT_VALUE + ], + expect: [200] + } + ] + }, + { + /** XAPI-00118, Data 4.1 Extensions + * An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an extension key which is not a valid IRI, if an extension object is present. + */ + name: 'An Extension "key" is an IRI (Format, Data 4.1.s3.b1, XAPI-00118)', + config: [ + { + name: 'statement activity extensions key is not an IRI', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'}, + {definition: INVALID_EXTENSION_KEY} + ], + expect: [400] + }, + { + name: 'statement result extensions key is not an IRI', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + INVALID_EXTENSION_KEY + ], + expect: [400] + }, + { + name: 'statement context extensions key is not an IRI', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + INVALID_EXTENSION_KEY + ], + expect: [400] + }, + { + name: 'statement substatement activity extensions key is not an IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'}, + {definition: INVALID_EXTENSION_KEY} + ], + expect: [400] + }, + { + name: 'statement substatement result extensions key is not an IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + INVALID_EXTENSION_KEY + ], + expect: [400] + }, + { + name: 'statement substatement context extensions key is not an IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'}, + INVALID_EXTENSION_KEY + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/formatting.js b/test/v2_0/configs/formatting.js new file mode 100644 index 00000000..f641ed0b --- /dev/null +++ b/test/v2_0/configs/formatting.js @@ -0,0 +1,826 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module, helper) { + "use strict"; + + // defines overwriting data + var INVALID_DATE = '01/011/2015'; + var INVALID_NUMERIC = 12345; + var INVALID_OBJECT = {key: 'should fail'}; + var INVALID_STRING = 'should fail'; + var INVALID_UUID_TOO_MANY_DIGITS = 'AA97B177-9383-4934-8543-0F91A7A028368'; + var INVALID_UUID_INVALID_LETTER = 'MA97B177-9383-4934-8543-0F91A7A02836'; + var VALID_EXTENSION = {extensions: {'http://example.com/null': null}}; + var INVALID_ACCOUNT_NAME_IRL = {account: {name: INVALID_OBJECT}}; + var VALID_ATTACHMENT = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'description': {'en-US': 'A test attachment (description)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }; + var VALID_ATTACHMENT_DISPLAY = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment', 'es': 'Un accesorio de prueba'}, + 'description': {'en-US': 'A test attachment (description)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }; + var VALID_ATTACHMENT_DESCRIPTION = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'description': {'en-US': 'A test attachment (description)', 'es-MX': 'Un accesorio de prueba (descripción)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }; + var INVALID_DESCRIPTION_ATTACHMENT = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'description': {'en-US': 'A test attachment (description)', 'something': 'Un accesorio de prueba (descripción)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }; + var INVALID_DISPLAY_ATTACHMENT = { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment', 'something': 'Un accesorio de prueba'}, + 'description': {'en-US': 'A test attachment (description)'}, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + }; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00003, 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request a Statement which does not contain an "actor" property + */ + name: 'A Statement contains an "actor" property (Multiplicity, Data 2.2.s2.b3, XAPI-00003)', + config: [ + { + name: 'statement "actor" missing', + templates: [ + {statement: '{{statements.no_actor}}'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00004, 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request a Statement which does not contain a "verb" property + */ + name: 'A Statement contains a "verb" property (Multiplicity, Data 2.2.s2.b3, XAPI-00004)', + config: [ + { + name: 'statement "verb" missing', + templates: [ + {statement: '{{statements.no_verb}}'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00005, 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request a Statement which does not contain an "object" property + */ + name: 'A Statement contains an "object" property (Multiplicity, Data 2.2.s2.b3, XAPI-00005)', + config: [ + { + name: 'statement "object" missing', + templates: [ + {statement: '{{statements.no_object}}'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00001, 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request any Statement having a property whose value is set to "null", an empty object, or has no value, except in an "extensions" property + */ + name: 'An LRS rejects with error code 400 Bad Request any Statement having a property whose value is set to "null", except in an "extensions" property (Data 2.2.s4.b1.b1, XAPI-00001)', + config: [ + { + name: 'statement actor should fail on "null"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox}}'}, + {name: null} + ], + expect: [400] + }, + { + name: 'statement verb should fail on "null"', + templates: [ + {statement: '{{statements.verb}}'}, + {verb: '{{verbs.default}}'}, + {display: {'en-US': null}} + ], + expect: [400] + }, + { + name: 'statement context should fail on "null"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {registration: null} + ], + expect: [400] + }, + { + name: 'statement object should fail on "null"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'}, + {definition: {moreInfo: null}} + ], + expect: [400] + }, + { + name: 'statement activity extensions can be empty', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: VALID_EXTENSION} + ], + expect: [200] + }, + { + name: 'statement result extensions can be empty', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.no_extensions}}'}, + VALID_EXTENSION + ], + expect: [200] + }, + { + name: 'statement context extensions can be empty', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + VALID_EXTENSION + ], + expect: [200] + }, + { + name: 'statement substatement activity extensions can be empty', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_extensions}}'}, + {definition: VALID_EXTENSION} + ], + expect: [200] + }, + { + name: 'statement substatement result extensions can be empty', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.no_extensions}}'}, + VALID_EXTENSION + ], + expect: [200] + }, + { + name: 'statement substatement context extensions can be empty', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.no_extensions}}'}, + VALID_EXTENSION + ], + expect: [200] + } + ] + }, + { + /** XAPI-00006, Data 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data type + */ + name: 'An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data type (Data 2.2.s4.b2, XAPI-00006)', + config: [ + { + name: 'with strings where numbers are required', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {max: 'one hundred'}} + ], + expect: [400] + }, + { + name: 'even if those strings contain numbers', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {max: '100'}} + ], + expect: [400] + }, + { + name: 'with strings where booleans are required', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {success: 'We regret to inform you that your effort was unsuccessful.'} + ], + expect: [400] + }, + { + name: 'even if those strings contain booleans', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {completion: 'false'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00007, Data 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request a Statement which uses any non-format-following key or value, including the empty string, where a string with a particular format (such as mailto IRI, UUID, or IRI) is required. + * Additional UUID tests in uuids.js xapi-00027 & 28 + * IFI's covered in actors.js xapi-0038 + */ + name: 'An LRS rejects with error code 400 Bad Request a Statement which uses any non-format-following key or value, including the empty string, where a string with a particular format, such as mailto IRI, UUID, or IRI, is required. (Data 2.2.s4.b4, XAPI-00007)', + config: [ + { + name: 'statement "id" invalid numeric', + templates: [ + {statement: '{{statements.default}}'}, + {id: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement "id" invalid object', + templates: [ + {statement: '{{statements.default}}'}, + {id: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement "id" invalid UUID with too many digits', + templates: [ + {statement: '{{statements.default}}'}, + {id: INVALID_UUID_TOO_MANY_DIGITS} + ], + expect: [400] + }, + { + name: 'statement "id" invalid UUID with non A-F', + templates: [ + {statement: '{{statements.default}}'}, + {id: INVALID_UUID_INVALID_LETTER} + ], + expect: [400] + }, + { + name: 'statement actor "agent" account "name" property is string', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement actor "group" account "name" property is string', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement authority "agent" account "name" property is string', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement authority "group" account "name" property is string', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement context instructor "agent" account "name" property is string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement context instructor "group" account "name" property is string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement context team "group" account "name" property is string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement substatement as "agent" account "name" property is string', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement substatement as "group" account "name" property is string', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s "agent" account "name" property is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s "group" account "name" property is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "agent" account "name" property is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "group" account "name" property is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "group" account "name" property is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_name}}'}, + INVALID_ACCOUNT_NAME_IRL + ], + expect: [400] + } + ] + }, + { + /** XAPI-00008, Data 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request a Statement where the case of a key does not match the case specified in this specification. + */ + /** XAPI-00010, Data 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request a Statement where a key or value is not allowed by this specification. + * This meets keys not allowed. Values not allowed are scattered throughout the suite. + */ + name: 'An LRS rejects with error code 400 Bad Request a Statement where the case of a key does not match the case specified in this specification. (Data 2.2.s4.b1.b5, XAPI-00008, XAPI-00010)', + config: [ + { + name: 'should fail when not using "id"', + templates: [ + {statement: '{{statements.default}}'}, + {iD: helper.generateUUID()} + ], + expect: [400] + }, + { + name: 'should fail when not using "actor"', + templates: [ + {statement: '{{statements.default}}'}, + {Actor: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'should fail when not using "verb"', + templates: [ + {statement: '{{statements.default}}'}, + {veRb: '{{verbs.default}}'} + ], + expect: [400] + }, + { + name: 'should fail when not using "object"', + templates: [ + {statement: '{{statements.default}}'}, + {oBject: "{{activities.default}}"} + ], + expect: [400] + }, + { + name: 'should fail when not using "result"', + templates: [ + {statement: '{{statements.result}}'}, + {RESULT: '{{results.default}}'} + ], + expect: [400] + }, + { + name: 'should fail when not using "context"', + templates: [ + {statement: '{{statements.context}}'}, + {conText: '{{contexts.default}}'} + ], + expect: [400] + }, + { + name: 'should fail when not using "timestamp"', + templates: [ + {statement: '{{statements.default}}'}, + {timeStamp: new Date("July 04, 1776 16:00:45.123456")} + ], + expect: [400] + }, + { + name: 'should fail when not using "stored"', + templates: [ + {statement: '{{statements.default}}'}, + {STOred: new Date("Sep 17, 1787 09:33:32:1111111")} + ], + expect: [400] + }, + { + name: 'should fail when not using "authority"', + templates: [ + {statement: '{{statements.authority}}'}, + {auTHORity: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'should fail when not using "version"', + templates: [ + {statement: '{{statements.default}}'}, + {Version: '2.0.0'} + ], + expect: [400] + }, + { + name: 'should fail when not using "attachments"', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachmentS: [VALID_ATTACHMENT]} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00009, Data 2.2 Formatting Requirements + * An LRS rejects with error code 400 Bad Request a Statement where the case of a value restricted to enumerated values does not match an enumerated value given in this specification exactly. + */ + name: 'An LRS rejects with error code 400 Bad Request a Statement where the case of a value restricted to enumerated values does not match an enumerated value given in this specification exactly. (Data 2.2.s4.b1.b6, XAPI-00009)', + config: [ + { + name: 'when interactionType is wrong case ("true-faLse")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.true_false}}'}, + {definition: {interactionType: "true-faLse"}} + ], + expect: [400] + }, + { + name: 'when interactionType is wrong case ("choiCe")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.choice}}'}, + {definition: {interactionType: "choiCe"}} + ], + expect: [400] + }, + { + name: 'when interactionType is wrong case ("fill-iN")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.fill_in}}'}, + {definition: {interactionType: "fill-iN"}} + ], + expect: [400] + }, + { + name: 'when interactionType is wrong case ("long-fiLl-in")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.long_fill_in}}'}, + {definition: {interactionType: "long-fiLl-in"}} + ], + expect: [400] + }, + { + name: 'when interactionType is wrong case ("matchIng")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.matching}}'}, + {definition: {interactionType: "matchIng"}} + ], + expect: [400] + }, + { + name: 'when interactionType is wrong case ("perfOrmance")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.performance}}'}, + {definition: {interactionType: "perfOrmance"}} + ], + expect: [400] + }, + { + name: 'when interactionType is wrong case ("seqUencing")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.sequencing}}'}, + {definition: {interactionType: "seqUencing"}} + ], + expect: [400] + }, + { + name: 'when interactionType is wrong case ("liKert")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.likert}}'}, + {definition: {interactionType: "liKert"}} + ], + expect: [400] + }, + { + name: 'when interactionType is wrong case ("nUmeric")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.numeric}}'}, + {definition: {interactionType: "nUmeric"}} + ], + expect: [400] + }, + { + name: 'when interactionType is wrong case ("Other")', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.other}}'}, + {definition: {interactionType: "Other"}} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00013 2.2 Formatting Requirements + * The LRS rejects with error code 400 Bad Request a token with does not validate as matching the RFC 5646 standard in the sequence of token lengths for language map keys. + */ + name: 'The LRS rejects with error code 400 Bad Request a token with does not validate as matching the RFC 5646 standard in the sequence of token lengths for language map keys. (Format, Data 2.2.s4.b2, Data 2.4.6.s3.table1.row7, RFC5646, XAPI-00013)', + config: [ + { + name: 'statement verb "display" should pass given de two letter language code only', + templates: [ + {statement: '{{statements.default}}'}, + {verb: '{{verbs.default}}'}, + {display: {de: 'besucht'}} + ], + expect: [200] + }, + { + name: 'statement verb "display" should fail given invalid language code', + templates: [ + {statement: '{{statements.default}}'}, + {verb: '{{verbs.default}}'}, + {display: {'something': 'besucht'}} + ], + expect: [400] + }, + { + name: 'statement object "name" should pass given de-DE language-region code', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.language_maps_valid_name}}'} + ], + expect: [200] + }, + { + name: 'statement object "name" should fail given invalid language code', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.name_invalid}}'} + ], + expect: [400] + }, + { + name: 'statement object "description" should pass given zh-Hant language-script code', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.language_maps_valid_description}}'} + ], + expect: [200] + }, + { + name: 'statement object "description" should fail given invalid language code', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.description_invalid}}'} + ], + expect: [400] + }, + { + name: 'interaction components\' description should pass given sr-Latn-RS language-script-region code', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.likert_valid_language_map}}'} + ], + expect: [200] + }, + { + name: 'context.language should pass given three letter cmn language code', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {language: 'cmn'} + ], + expect: [200] + }, + { + name: 'context.language should fail given invalid language code', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {language: 'something'} + ], + expect: [400] + }, + { + name: 'statement attachment "display" should pass given es two letter language code only', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [VALID_ATTACHMENT_DISPLAY]} + ], + expect: [200] + }, + { + name: 'statement attachment "display" should fail given invalid language code', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [INVALID_DISPLAY_ATTACHMENT]} + ], + expect: [400] + }, + { + name: 'statement attachment "description" should pass given es-MX language-UN region code', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [VALID_ATTACHMENT_DESCRIPTION]} + ], + expect: [200] + }, + { + name: 'statement attachment "description" should fail given es-MX invalid language code', + templates: [ + {statement: '{{statements.attachment}}'}, + {attachments: [INVALID_DESCRIPTION_ATTACHMENT]} + ], + expect: [400] + }, + { + name: 'statement substatement verb "display" should pass given sr-Cyrl language-script code', + templates: [ + {statement: '{{statements.object_substatement_default}}'}, + {object: '{{substatements.verb_valid}}'} + ], + expect: [200] + }, + { + name: 'statement substatement verb "display" should fail given invalid language code', + templates: [ + {statement: '{{statements.object_substatement_default}}'}, + {object: '{{substatements.verb_invalid}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "name" should pass given zh-Hans-CN language-script-region code', + templates: [ + {statement: '{{statements.object_substatement_default}}'}, + {object: '{{substatements.activity_definition_name}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "name" should fail given zh-z-aaa-z-bbb-c-ccc invalid (two extensions with same single-letter prefix) language code', + templates: [ + {statement: '{{statements.object_substatement_default}}'}, + {object: '{{substatements.activity_definition_name_invalid}}'} + ], + expect: [400] + }, + { + name: 'statement substatement activity "description" should pass given ase three letter language code', + templates: [ + {statement: '{{statements.object_substatement_default}}'}, + {object: '{{substatements.activity_definition_description}}'} + ], + expect: [200] + }, + { + name: 'statement substatement activity "description" should fail given invalid language code', + templates: [ + {statement: '{{statements.object_substatement_default}}'}, + {object: '{{substatements.activity_definition_description_invalid}}'} + ], + expect: [400] + }, + { + name: 'substatement interaction components\' description should pass given ja two letter language code only', + templates: [ + {statement: '{{statements.object_substatement_default}}'}, + {object: '{{substatements.interaction_component_valid_language_map}}'} + ], + expect: [200] + }, + { + name: 'substatement context.language should pass given two letter fr-CA language-region code', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context_valid_language_map}}'} + ], + expect: [200] + }, + { + name: 'substatement context.language should fail given invalid language code', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context_invalid_language_map}}'} + ], + expect: [400] + } + ] + } + ]; + }; +}(module, require('./../../helper.js'))); diff --git a/test/v2_0/configs/groups.js b/test/v2_0/configs/groups.js new file mode 100644 index 00000000..c42cc4b3 --- /dev/null +++ b/test/v2_0/configs/groups.js @@ -0,0 +1,1674 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var FOREIGN_IDENTIFIER_ACCOUNT = {'account': {'homePage': 'http://www.example.com', 'name': 'xAPI account name'}}; + var FOREIGN_IDENTIFIER_MBOX = {'mbox': 'mailto:xapi@adlnet.gov'}; + var FOREIGN_IDENTIFIER_MBOX_SHA1SUM = {'mbox_sha1sum': 'cd9b00a5611f94eaa7b1661edab976068e364975'}; + var FOREIGN_IDENTIFIER_OPENID = {'openid': 'http://openid.example.org/12345'}; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00035, 2.4.2.2 when the actor objectType is group + * A Group uses the "member" property. An LRS rejects with 400 Bad Request if the "member" property is present anywhere but in a group object (Actor or team). + */ + name: 'An Anonymous Group uses the "member" property (Multiplicity, Data 2.4.2.2.s2.table1.row3, XAPI-00035)', + config: [ + { + name: 'statement actor anonymous group missing member', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.anonymous_no_member}}'} + ], + expect: [400] + }, + { + name: 'statement authority anonymous group missing member', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.default}}'}, + {member: '{{groups.anonymous_no_member}}'} + ], + expect: [400] + }, + { + name: 'statement context instructor anonymous group missing member', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.default}}'}, + {member: '{{groups.anonymous_no_member}}'} + ], + expect: [400] + }, + { + name: 'statement context team anonymous group missing member', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.default}}'}, + {member: '{{groups.anonymous_no_member}}'} + ], + expect: [400] + }, + { + name: 'statement substatement as group anonymous group missing member', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.default}}'}, + {member: '{{groups.anonymous_no_member}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s group anonymous group missing member', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.default}}'}, + {member: '{{groups.anonymous_no_member}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor anonymous group missing member', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.default}}'}, + {member: '{{groups.anonymous_no_member}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context team anonymous group missing member', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.default}}'}, + {member: '{{groups.anonymous_no_member}}'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00036, Data 2.4.2.2 when the actor objectType is group + * The "member" property is an array of Objects following Agent requirements. An LRS rejects with 400 Bad Request any group object which has a member property with anything other than a valid array of Agents as a value + */ + name: 'The "member" property is an array of Objects following Agent requirements (Data 2.4.2.2.s2.table2.row3, XAPI-00036)', + config: [ + { + name: 'statement actor requires member type "array"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.default}}'}, + {member: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'statement authority requires member type "array"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.default}}'}, + {member: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'statement context instructor requires member type "array"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.default}}'}, + {member: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'statement context team requires member type "array"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.default}}'}, + {member: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'statement substatement as group requires member type "array"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.default}}'}, + {member: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s group requires member type "array"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.default}}'}, + {member: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor requires member type "array"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.default}}'}, + {member: '{{agents.default}}'} + ], + expect: [400] + }, + { + name: 'statement substatement"s context team requires member type "array"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.default}}'}, + {member: '{{agents.default}}'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00037, Data 2.4.2.2 when actor objectType is group + * An "actor" property with "objectType" as "Group" uses exactly one of the following Inverse Functional Identifier properties: "mbox", "mbox_sha1sum", "openid", "account" or a member property with at least one Agent. An LRS rejects with 400 Bad Request any group object with: + - no IFI and no member property + - more than one IFI + - an invalid IFI value + * The remaining 6 suites take care of XAPI-00037 + */ + name: 'An Identified Group is defined by "objectType" of an "actor" or "object" with value "Group" and by one of "mbox", "mbox_sha1sum", "openid", or "account" being used (Data 2.4.2.2.s2.table2.row1, XAPI-00037)', + config: [ + { + name: 'statement actor identified group accepts "mbox"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox}}'} + ], + expect: [200] + }, + { + name: 'statement actor identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement actor identified group accepts "openid"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_openid}}'} + ], + expect: [200] + }, + { + name: 'statement actor identified group accepts "account"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor identified group accepts "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor identified group accepts "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor identified group accepts "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement context team identified group accepts "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox}}'} + ], + expect: [200] + }, + { + name: 'statement context team identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement context team identified group accepts "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid}}'} + ], + expect: [200] + }, + { + name: 'statement context team identified group accepts "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group identified group accepts "mbox"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group identified group accepts "openid"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_openid}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group identified group accepts "account"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group identified group accepts "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group identified group accepts "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_openid}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group identified group accepts "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor identified group accepts "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor identified group accepts "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor identified group accepts "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team identified group accepts "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team identified group accepts "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team identified group accepts "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account}}'} + ], + expect: [200] + } + ] + }, + { //see above + name: 'An Identified Group uses one of the following properties: "mbox", "mbox_sha1sum", "openid", "account" (Multiplicity, Data 2.4.2.2.s2.table2.row4, XAPI-00037)', + config: [ + { + name: 'statement actor identified group accepts "mbox"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement actor identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement actor identified group accepts "openid"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_openid_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement actor identified group accepts "account"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor identified group accepts "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor identified group accepts "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor identified group accepts "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement context team identified group accepts "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement context team identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement context team identified group accepts "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement context team identified group accepts "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group identified group accepts "mbox"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox_sha1sum_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group identified group accepts "openid"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_openid_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group identified group accepts "account"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group identified group accepts "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group identified group accepts "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_openid_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group identified group accepts "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_account_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor identified group accepts "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor identified group accepts "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor identified group accepts "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team identified group accepts "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team identified group accepts "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team identified group accepts "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid_no_member}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team identified group accepts "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account_no_member}}'} + ], + expect: [200] + } + ] + }, + { //see above + name: 'An Identified Group does not use the "mbox" property if "mbox_sha1sum", "openid", or "account" are used (Multiplicity, Data 2.4.2.2.s5.b1, XAPI-00037)', + config: [ + { + name: 'statement actor "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement actor "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement actor "mbox" cannot be used with "openid', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement authority "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement authority "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement authority "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement context team "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement context team "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement context team "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement as group "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement as group "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement as group "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {actor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "mbox" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "mbox" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "mbox" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + } + ] + }, + { //see above + name: 'An Identified Group does not use the "mbox_sha1sum" property if "mbox", "openid", or "account" are used (Multiplicity, Data 2.4.2.2.s5.b1, XAPI-00037)', + config: [ + { + name: 'statement actor "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement actor "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement actor "mbox_sha1sum" cannot be used with "openid', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement authority "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement authority "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement authority "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement context instructor "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement context team "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement context team "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement context team "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement as group "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement as group "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement as group "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "mbox_sha1sum" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "mbox_sha1sum" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "mbox_sha1sum" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_mbox_sha1sum}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + } + ] + }, + { //see above + name: 'An Identified Group does not use the "openid" property if "mbox", "mbox_sha1sum", or "account" are used (Multiplicity, Data 2.4.2.2.s5.b1, XAPI-00037)', + config: [ + { + name: 'statement actor "openid" cannot be used with "account', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement actor "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement actor "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement authority "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement authority "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement authority "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement context instructor "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement context instructor "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement context instructor "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement context team "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement context team "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement context team "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement as group "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement as group "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement as group "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {actor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "openid" cannot be used with "account"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_ACCOUNT + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "openid" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "openid" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_openid}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + } + ] + }, + { //see above + name: 'An Identified Group does not use the "account" property if "mbox", "mbox_sha1sum", or "openid" are used (Multiplicity, Data 2.4.2.2.s5.b1, XAPI-00037)', + config: [ + { + name: 'statement actor "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement actor "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement actor "account" cannot be used with "openid', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement authority "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement authority "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement authority "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement context instructor "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement context instructor "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement context instructor "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement context team "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement context team "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement context team "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement as group "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement as group "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement as group "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {actor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s agent "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "account" cannot be used with "mbox"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "account" cannot be used with "mbox_sha1sum"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_MBOX_SHA1SUM + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "account" cannot be used with "openid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.identified_account}}'}, + FOREIGN_IDENTIFIER_OPENID + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/ifis.js b/test/v2_0/configs/ifis.js new file mode 100644 index 00000000..bae1abae --- /dev/null +++ b/test/v2_0/configs/ifis.js @@ -0,0 +1,731 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_OBJECT = {key: 'value'}; + var INVALID_OBJECTTYPE_NUMERIC = {objectType: 123}; + var INVALID_OBJECTTYPE_OBJECT = {objectType: INVALID_OBJECT}; + var INVALID_OBJECTTYPE_NAME_NUMERIC = {name: 123}; + var INVALID_OBJECTTYPE_NAME_OBJECT = {name: INVALID_OBJECT}; + var INVALID_MAIL_TO_EMAIL = 'mailto:should.fail.com'; + var INVALID_MAIL_TO_IRI = 'http://should.fail.com'; + var INVALID_URI = 'ab=c://should.fail.com'; + var INVALID_ACCOUNT_HOMEPAGE_IRL = {account: {homePage: INVALID_URI}}; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00038, Data 2.4.2.3 Inverse Functional Identifier + * An "mbox" property has the form "mailto:email address" and is an IRI. An LRS rejects with 400 Bad Request if a statement that uses the “mbox” IFI is an invalid form. + */ + name: 'An "mbox" property has the form "mailto:email address" and is an IRI (Type, Data 2.4.2.3.s3.table1.row1, XAPI-00038)', + config: [ + { + name: 'statement actor "agent mbox" not IRI', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement actor "group mbox" not IRI', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement authority "agent mbox" not IRI', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement authority "group mbox" not IRI', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement context instructor "agent mbox" not IRI', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement context instructor "group mbox" not IRI', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement context team "group mbox" not IRI', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement substatement as "agent mbox" not IRI', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement substatement as "group mbox" not IRI', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement substatement"s "agent mbox" not IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement substatement"s "group mbox" not IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "agent mbox" not IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "group mbox" not IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "group mbox" not IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_IRI} + ], + expect: [400] + }, + { + name: 'statement actor "agent mbox" not mailto:email address', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement actor "group mbox" not mailto:email address', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement authority "agent mbox" not mailto:email address', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement authority "group mbox" not mailto:email address', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement context instructor "agent mbox" not mailto:email address', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement context instructor "group mbox" not mailto:email address', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement context team "group mbox" not mailto:email address', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement substatement as "agent mbox" not mailto:email address', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement substatement as "group mbox" not mailto:email address', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement substatement"s "agent mbox" not mailto:email address', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement substatement"s "group mbox" not mailto:email address', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "agent mbox" not mailto:email address', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "group mbox" not mailto:email address', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "group mbox" not mailto:email address', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox}}'}, + {mbox: INVALID_MAIL_TO_EMAIL} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00039, Data 2.4.2.3 Inverse Functional Identifier + * An "mbox_sha1sum" property is a String An LRS rejects with 400 Bad Request if a statement uses the “mbox_sha1sum” IFI and it is not a valid string. + */ + name: 'An "mbox_sha1sum" property is a String (Type, Data 2.4.2.3.s3.table1.row2, XAPI-00039)', + config: [ + { + name: 'statement actor "agent mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement actor "group mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement authority "agent mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement authority "group mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement context instructor "agent mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement context instructor "group mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement context team "group mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement as "agent mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement as "group mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement"s "agent mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement"s "group mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "agent mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "group mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "group mbox_sha1sum" not string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_mbox_sha1sum}}'}, + {mbox_sha1sum: INVALID_OBJECT} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00040, Data 2.4.2.3 Inverse Functional Identifier + * An "openid" property is a URI. An LRS rejects with 400 Bad Request if a statement uses the “openID” IFI and the URI is invalid. + */ + name: 'An "openid" property is a URI (Type, Data 2.4.2.3.s3.table1.row3, XAPI-00040)', + config: [ + { + name: 'statement actor "agent openid" not URI', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement actor "group openid" not URI', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement authority "agent openid" not URI', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement authority "group openid" not URI', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.identified_openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement context instructor "agent openid" not URI', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement context instructor "group openid" not URI', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement context team "group openid" not URI', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement substatement as "agent openid" not URI', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement substatement as "group openid" not URI', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement substatement"s "agent openid" not URI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement substatement"s "group openid" not URI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "agent openid" not URI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement substatement"s context instructor "group openid" not URI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement substatement"s context team "group openid" not URI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_openid}}'}, + {openid: INVALID_URI} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00041, Data 2.4.2.3 Inverse Functional Identifier + * An “account” property is an object. An LRS rejects with 400 Bad Request if a statement uses an invalid Account Object. A valid account is defined by the requirements listed in XAPI-I-63 and XAPI-I-66 + * Covers next suite + */ + name: 'An Account Object is the "account" property of a Group or Agent (Definition, Data 2.4.2.4, XAPI-00041)', + config: [ + { + name: 'statement actor "agent account" property exists', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement actor "group account" property exists', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement authority "agent account" property exists', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement authority "group account" property exists', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.authority_group}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor "agent account" property exists', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor "group account" property exists', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement context team "group account" property exists', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as "agent account" property exists', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as "group account" property exists', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s "agent account" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s "group account" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor "agent account" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor "group account" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team "group account" property exists', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.identified_account}}'} + ], + expect: [200] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/languages.js b/test/v2_0/configs/languages.js new file mode 100644 index 00000000..2c8f0c30 --- /dev/null +++ b/test/v2_0/configs/languages.js @@ -0,0 +1,127 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_LANGUAGE = {a12345678: 'should error'}; + var INVALID_DESCRIPTION_LANGUAGE = {description: INVALID_LANGUAGE}; + var INVALID_DISPLAY_LANGUAGE = {display: INVALID_LANGUAGE}; + var INVALID_NAME_LANGUAGE = {name: INVALID_LANGUAGE}; + + // configures tests + module.exports.config = function () { + return [ + + { + /** XAPI-00121, Data 4.2 Language Maps + * A Language Map follows RFC 5646. The LRS rejects with 400 a statement using a Language Map which doesn’t not validate to RFC 5646. + */ + name: 'A Language Map follows RFC5646 (Format, Data 4.2.s1, RFC5646, XAPI-00121)', + config: [ + { + name: 'statement verb "display" language map invalid', + templates: [ + {statement: '{{statements.verb}}'}, + {verb: '{{verbs.no_display}}'}, + INVALID_DISPLAY_LANGUAGE + ], + expect: [400] + }, + { + name: 'statement object "name" language map invalid', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_languages}}'}, + {definition: INVALID_NAME_LANGUAGE} + ], + expect: [400] + }, + { + name: 'statement object "description" language map invalid', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_languages}}'}, + {definition: INVALID_DESCRIPTION_LANGUAGE} + ], + expect: [400] + }, + { + name: 'statement attachment "display" language map invalid', + templates: [ + {statement: '{{statements.attachment}}'}, + { + attachments: [ + { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': INVALID_LANGUAGE, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + } + ] + } + ], + expect: [400] + }, + { + name: 'statement attachment "description" language map invalid', + templates: [ + {statement: '{{statements.attachment}}'}, + { + attachments: [ + { + 'usageType': 'http://example.com/attachment-usage/test', + 'display': {'en-US': 'A test attachment'}, + 'description': INVALID_LANGUAGE, + 'contentType': 'text/plain; charset=ascii', + 'length': 27, + 'sha2': '495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a', + 'fileUrl': 'http://over.there.com/file.txt' + } + ] + } + ], + expect: [400] + }, + { + name: 'statement substatement verb "display" language map invalid', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.verb}}'}, + {verb: '{{verbs.no_display}}'}, + INVALID_DISPLAY_LANGUAGE + ], + expect: [400] + }, + { + name: 'statement substatement activity "name" language map invalid', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_languages}}'}, + {definition: INVALID_NAME_LANGUAGE} + ], + expect: [400] + }, + { + name: 'statement substatement activity "description" language map invalid', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_languages}}'}, + {definition: INVALID_DESCRIPTION_LANGUAGE} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/objects.js b/test/v2_0/configs/objects.js new file mode 100644 index 00000000..fe8b48b4 --- /dev/null +++ b/test/v2_0/configs/objects.js @@ -0,0 +1,115 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_ACTIVITY = 'activity'; + var INVALID_AGENT = 'agent'; + var INVALID_GROUP = 'group'; + var INVALID_STATEMENTREF = 'statementref'; + var INVALID_SUBSTATEMENT = 'substatement'; + + // configures tests + module.exports.config = function () { + return [ + + { + /** XAPI-00046, Data 2.4.4 Object + * An "object" property's "objectType" property is either "Activity", "Agent", "Group", "SubStatement", or "StatementRef". An LRS rejects with 400 Bad Request an “object” property with an objectType which is not "Activity", "Agent", "Group", "SubStatement", or "StatementRef". + */ + name: 'An "object" property\'s "objectType" property is either "Activity", "Agent", "Group", "SubStatement", or "StatementRef" (Vocabulary, Data 2.4.4.s2, XAPI-00046)', + config: [ + { + name: 'statement activity should fail on "activity"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'}, + {objectType: INVALID_ACTIVITY} + ], + expect: [400] + }, + { + name: 'statement substatement activity should fail on "activity"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'}, + {objectType: INVALID_ACTIVITY} + ], + expect: [400] + }, + { + name: 'statement agent template should fail on "agent"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'}, + {objectType: INVALID_AGENT} + ], + expect: [400] + }, + { + name: 'statement substatement agent should fail on "agent"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.agent}}'}, + {object: '{{agents.default}}'}, + {objectType: INVALID_AGENT} + ], + expect: [400] + }, + { + name: 'statement group should fail on "group"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.default}}'}, + {objectType: INVALID_GROUP} + ], + expect: [400] + }, + { + name: 'statement substatement group should fail on "group"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.group}}'}, + {object: '{{groups.default}}'}, + {objectType: INVALID_GROUP} + ], + expect: [400] + }, + { + name: 'statement StatementRef should fail on "statementref"', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {objectType: INVALID_STATEMENTREF} + ], + expect: [400] + }, + { + name: 'statement substatement StatementRef should fail on "statementref"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'}, + {objectType: INVALID_STATEMENTREF} + ], + expect: [400] + }, + { + name: 'statement SubStatement should fail on "substatement"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {objectType: INVALID_SUBSTATEMENT} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/results.js b/test/v2_0/configs/results.js new file mode 100644 index 00000000..3f7cdc50 --- /dev/null +++ b/test/v2_0/configs/results.js @@ -0,0 +1,302 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_DURATION = 'PA1H0M0S'; + var INVALID_NUMERIC = 12345; + var INVALID_OBJECT = {key: 'invalid'}; + var INVALID_STRING = 'should fail'; + var VALID_DURATION = 'PT1H0M0.1S'; + var VALID_DECIMAL_DIGITS = .6767676; + var VALID_MAX_DECIMAL_DIGITS = 100.6767676; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00074, Data 2.4.5 result + * A "success" property is a Boolean. The LRS rejects with 400 Bad Request a Statement which has a Result Object with a “success” property which does not have a valid Boolean value, if present. + */ + name: 'A "success" property is a Boolean (Type, Data 2.4.5.s2.table1.row1, XAPI-00074)', + config: [ + { + name: 'statement result "success" property is string "true"', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {success: 'true'} + ], + expect: [400] + }, + { + name: 'statement result "success" property is string "false"', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {success: 'false'} + ], + expect: [400] + }, + { + name: 'statement substatement result "success" property is string "true"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {success: 'true'} + ], + expect: [400] + }, + { + name: 'statement substatement result "success" property is string "false"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {success: 'false'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00075, Data 2.4.5 result + * A "completion" property is a Boolean. The LRS rejects with 400 Bad Request a Statement which has a Result Object with a “completion” property which does not have a valid Boolean value, if present. + */ + name: 'A "completion" property is a Boolean (Type, Data 2.4.5.s2.table1.row2, XAPI-00075)', + config: [ + { + name: 'statement result "completion" property is string "true"', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {completion: 'true'} + ], + expect: [400] + }, + { + name: 'statement result "completion" property is string "false"', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {completion: 'false'} + ], + expect: [400] + }, + { + name: 'statement substatement result "completion" property is string "true"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {completion: 'true'} + ], + expect: [400] + }, + { + name: 'statement substatement result "completion" property is string "false"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {completion: 'false'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00076, Data 2.4.5 result + * A "response" property is a String. The LRS rejects with 400 Bad Request a Statement which has a Result Object with a “response” property which does not have a valid String value, if present. + */ + name: 'A "response" property is a String (Type, Data 2.4.5.s2.table1.row3, XAPI-00076)', + config: [ + { + name: 'statement result "response" property is numeric', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {response: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement result "completion" property is object', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {response: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement substatement result "completion" property is numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {response: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement substatement result "completion" property is object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {response: INVALID_OBJECT} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00077, Data 2.4.5 result + * A "duration" property is a formatted to ISO 8601 durations (see Data 4.6). The LRS rejects with 400 Bad Request a Statement which has a Result Object with a “duration” property which does not have a valid ISO 8601 value, if present. + */ + name: 'A "duration" property is a formatted to ISO 8601 (Type, Data 2.4.5.s2.table1.row4, XAPI-00077)', + config: [ + { + name: 'statement result "duration" property is invalid', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_DURATION} + ], + expect: [400] + }, + { + name: 'statement substatement result "duration" property is invalid', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_DURATION} + ], + expect: [400] + }, + { + name: 'statement result "duration" property is valid', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: VALID_DURATION} + ], + expect: [200] + }, + { + name: 'statement substatement result "duration" property is valid', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: VALID_DURATION} + ], + expect: [200] + }, + { + name: 'statement result "duration" property is valid', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'PT4H35M59.14S'} + ], + expect: [200] + }, + { + name: 'statement substatement result "duration" property is valid', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'PT16559.14S'} + ], + expect: [200] + }, + { + name: 'statement result "duration" property is valid', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'P3Y1M29DT4H35M59.14S'} + ], + expect: [200] + }, + { + name: 'statement substatement result "duration" property is valid', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'P3Y'} + ], + expect: [200] + }, + { + name: 'statement result "duration" property is valid', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: 'P4W'} + ], + expect: [200] + } + ] + }, + { + /** XAPI-00078, Data 2.4.5 result + * An "extensions" property is an Object. The LRS rejects with 400 Bad Request a Statement which has a Result Object with aa “extensions” property which does not have a valid Extensions Object, if present. + */ + name: 'An "extensions" property is an Object (Type, Data 2.4.5.s2.table1.row6, XAPI-00078)', + config: [ + { + name: 'statement result "extensions" property is numeric', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement result "extensions" property is string', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement substatement result "extensions" property is numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement substatement result "extensions" property is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {duration: INVALID_STRING} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/scores.js b/test/v2_0/configs/scores.js new file mode 100644 index 00000000..3817a50e --- /dev/null +++ b/test/v2_0/configs/scores.js @@ -0,0 +1,294 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_DURATION = 'PA1H0M0S'; + var INVALID_NUMERIC = 12345; + var INVALID_OBJECT = {key: 'invalid'}; + var INVALID_STRING = 'should fail'; + var VALID_DURATION = 'PT1H0M0.1S'; + var VALID_DECIMAL_DIGITS = .6767676; + var VALID_MAX_DECIMAL_DIGITS = 100.6767676; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00079, Data 2.4.5.1 score + * A "score" property is an Object. The LRS rejects with 400 Bad Request a “score” property which is not a valid object. + */ + name: 'A "score" property is an Object (Type, Data 2.4.5.1, XAPI-00079)', + config: [ + { + name: 'statement result score numeric', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement result score string', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement substatement result score numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement substatement result score string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: INVALID_STRING} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00083, Data 2.4.5.1 score + * If the "score" Object uses the "scaled" property, the value must be a decimal number between -1 and 1. The LRS rejects with 400 Bad Request a statement with a Result Object using the “scaled” property (if it is present) which is not a decimal number or is greater than 1 or less than -1. + */ + name: 'A "score" Object\'s "scaled" property is a decimal number between -1 and 1, inclusive. (Type, Data 2.4.5.1.s2.table1.row1, XAPI-00083)', + config: [ + { + name: 'statement result "scaled" accepts decimal', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {scaled: VALID_DECIMAL_DIGITS}} + ], + expect: [200] + }, + { + name: 'statement substatement result "scaled" accepts decimal', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {scaled: VALID_DECIMAL_DIGITS}} + ], + expect: [200] + }, + { + name: 'statement result "scaled" should pass with value 1.0', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {scaled: 1.0}} + ], + expect: [200] + }, + { + name: 'statement substatement result "scaled" pass with value -1.0000', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {scaled: -1.0000}} + ], + expect: [200] + }, + { + name: 'statement result "scaled" should reject with value 1.01', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {scaled: 1.01}} + ], + expect: [400] + }, + { + name: 'statement substatement result "scaled" reject with value -1.00001', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {scaled: -1.00001}} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00082, Data 2.4.5.1 score + * If the "score" Object uses the "raw" property, the value must be a decimal number between the "min" and "max", if they are present. If they are not present "raw" can be any number. The LRS rejects with 400 Bad Request a statement with a Result Object using the “raw” property (if it is present) which is not a decimal number or is greater than the value of the “max” property, if it is present, or lesser than the value of the “min” property, if it is present. + */ + name: 'A "score" Object\'s "raw" property is a decimal number between min and max, if present and otherwise unrestricted, inclusive (Type, Data 2.4.5.1.s2.table1.row2, XAPI-00082)', + config: [ + { + name: 'statement result "raw" accepts decimal', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {raw: VALID_DECIMAL_DIGITS}} + ], + expect: [200] + }, + { + name: 'statement substatement result "raw" accepts decimal', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {raw: VALID_DECIMAL_DIGITS}} + ], + expect: [200] + }, + { + name: 'statement result "raw" rejects raw greater than max', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {raw: VALID_DECIMAL_DIGITS, max: VALID_DECIMAL_DIGITS - 0.02}} + ], + expect: [400] + }, + { + name: 'statement substatement result "raw" rejects raw greater than max', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {raw: VALID_DECIMAL_DIGITS, max: VALID_DECIMAL_DIGITS - 2}} + ], + expect: [400] + }, + { + name: 'statement result "raw" rejects raw less than min', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {raw: VALID_DECIMAL_DIGITS, min: VALID_DECIMAL_DIGITS + 0.73}} + ], + expect: [400] + }, + { + name: 'statement substatement result "raw" rejects raw less than min', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {raw: VALID_DECIMAL_DIGITS, min: VALID_DECIMAL_DIGITS + 7}} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00081, Data 2.4.5.1 score + * If the "score" Object uses the "min" property, the value must be a decimal number less than the "max" property, if it is present. If "max" is not present "min" can be any number. The LRS rejects with 400 Bad Request a statement with a Result Object using the “min” property (if it is present) which is not a decimal number or is greater than the value of the “max” property, if it is present. + */ + name: 'A "score" Object\'s "min" property is a decimal number less than the "max" property, if it is present. (Type, Data 2.4.5.1.s2.table1.row3, XAPI-00081)', + config: [ + { + name: 'statement result "min" accepts decimal', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {min: VALID_DECIMAL_DIGITS}} + ], + expect: [200] + }, + { + name: 'statement substatement result "min" accepts decimal', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {min: VALID_DECIMAL_DIGITS}} + ], + expect: [200] + }, + { + name: 'statement result "min" rejects decimal number greater than "max"', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {min: VALID_DECIMAL_DIGITS, max: VALID_DECIMAL_DIGITS - 0.0000321, raw: VALID_DECIMAL_DIGITS - 0.0000033}} + ], + expect: [400] + }, + { + name: 'statement substatement result "min" rejects decimal number greater than "max"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {min: VALID_DECIMAL_DIGITS, max: VALID_DECIMAL_DIGITS - 4, raw: VALID_DECIMAL_DIGITS - 1}} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00080, Data 2.4.5.1 score + * If the "score" Object uses the "max" property, the value must be a decimal number more than the "min" property, if it is present. If "min" is not present "max" can be any number. The LRS rejects with 400 Bad Request a statement with a Result Object using the “max” property (if it is present) which is not a decimal number or is lesser than the value of the “min” property, if it is present. + * If this is the test, this will need to be moved, so that the result can be checked, oh no now that i read closer, no get and check it needed just a couple more tests sending in particular configurations of min and max and expecting 400's or 200's + */ + name: 'A "score" Object\'s "max" property is a Decimal accurate to seven significant decimal figures (Type, Data 2.4.5.1.s2.table1.row4, XAPI-00080)', + config: [ + { + name: 'statement result "max" accepts a decimal number more than the "min" property, if it is present.', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {max: VALID_MAX_DECIMAL_DIGITS}} + ], + expect: [200] + }, + { + name: 'statement substatement result "max" accepts a decimal number more than the "min" property, if it is present.', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {max: VALID_MAX_DECIMAL_DIGITS}} + ], + expect: [200] + }, + { + name: 'statement result "max" accepts a decimal number more than the "min" property, if it is present.', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'}, + {score: {max: VALID_MAX_DECIMAL_DIGITS, min: VALID_MAX_DECIMAL_DIGITS + 4, raw: VALID_MAX_DECIMAL_DIGITS + 1}} + ], + expect: [400] + }, + { + name: 'statement substatement result "max" accepts a decimal number more than the "min" property, if it is present.', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'}, + {score: {max: VALID_MAX_DECIMAL_DIGITS, raw: VALID_MAX_DECIMAL_DIGITS + 10, min: VALID_MAX_DECIMAL_DIGITS + 100}} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/statementrefs.js b/test/v2_0/configs/statementrefs.js new file mode 100644 index 00000000..617d167e --- /dev/null +++ b/test/v2_0/configs/statementrefs.js @@ -0,0 +1,90 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_STATEMENT_REF = 'statementref'; + var INVALID_STRING = 'should fail'; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00073, Data 2.4.4.3 when object is a statement + * Statements that have a StatementRef or Sub-Statement as an Object MUST specify an "objectType" property. The LRS rejects with 400 Bad Request if the “objectType” property is absent and the Object is a StatementRef or Sub-Statement. + */ + name: 'A Statement Reference is defined by the "objectType" of an "object" with value "StatementRef" (Data 2.4.4.3.s4.b1, XAPI-00073)', + config: [ + { + name: 'statementref invalid when not "StatementRef"', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {object: {'objectType': INVALID_STATEMENT_REF}} + ], + expect: [400] + }, + { + name: 'substatement statementref invalid when not "StatementRef"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'}, + {object: {'objectType': INVALID_STATEMENT_REF}} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00072, Data 2.4.4.3 when object is a statement + * A Statement Reference's "id" property is a UUID. The LRS rejects with 400 Bad Request a Statement Reference Object with an “id” property which is absent or an invalid UUID. + */ + name: 'A Statement Reference contains an "id" property (Multiplicity, Data 2.4.4.3.s4.table1.row2, XAPI-00072)', + config: [ + { + name: 'statementref invalid when missing "id"', + templates: [ + {statement: '{{statements.object_statementref_no_id}}'} + ], + expect: [400] + }, + { + name: 'substatement statementref invalid when missing "id"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref_no_id}}'} + ], + expect: [400] + } + ] + }, + { //see above + name: 'A Statement Reference\'s "id" property is a UUID (Type, Data 2.4.4.3.s4.table1.row2, XAPI-00072)', + config: [ + { + name: 'statementref "id" not "uuid"', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {object: {'id': INVALID_STRING}} + ], + expect: [400] + }, + { + name: 'substatement statementref "id" not "uuid"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'}, + {object: {'id': INVALID_STRING}} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/substatements.js b/test/v2_0/configs/substatements.js new file mode 100644 index 00000000..64a183d7 --- /dev/null +++ b/test/v2_0/configs/substatements.js @@ -0,0 +1,191 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // configures tests + module.exports.config = function () { + return [ + { + name: 'A Sub-Statement is defined by the "objectType" of an "object" with value "SubStatement" (Data 2.4.4.3.s8.b1)', + config: [ + { + name: 'substatement invalid when not "SubStatement"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {objectType: 'substatement'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00066, Data 2.4.4.3 when object is a statement + * A Sub-Statement follows the requirements of all Statements. The LRS rejects with 400 Bad Request a Statement with a Sub-Statement with any invalid statement properties in an otherwise valid sub-statement. + */ + name: 'A Sub-Statement follows the requirements of all Statements (Data 2.4.4.3.s8.b2, XAPI-00066)', + config: [ + { + name: 'substatement requires actor', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.no_actor}}'} + ], + expect: [400] + }, + { + name: 'substatement requires object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.no_object}}'} + ], + expect: [400] + }, + { + name: 'substatement requires verb', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.no_verb}}'} + ], + expect: [400] + }, + { + name: 'should pass substatement context', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'} + ], + expect: [200] + }, + { + name: 'should pass substatement result', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'} + ], + expect: [200] + }, + { + name: 'should pass substatement statementref', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'} + ], + expect: [200] + }, + { + name: 'should pass substatement as agent', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.agent}}'}, + {object: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'should pass substatement as group', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.group}}'}, + {object: '{{groups.default}}'} + ], + expect: [200] + } + ] + }, + { + /** XAPI-00071, Data 2.4.4.3 when object is a statement + * A Sub-Statement cannot have a Sub-Statement. The LRS rejects with 400 Bad Request a Statement with a Sub-Statement which contains a Sub-Statement. + */ + name: 'A Sub-Statement cannot have a Sub-Statement (Data 2.4.4.3.s8.b4, XAPI-00071)', + config: [ + { + name: 'substatement invalid nested "SubStatement"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{statements.object_substatement}}'}, + {object: '{{statements.object_substatement_default}}'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00070, Data 2.4.4.3 when object is a statement + * A Sub-Statement cannot use the "id" property at the Statement level The LRS rejects with 400 Bad Request a Statement with a Sub-Statement where the “id” property is present. + */ + name: 'A Sub-Statement cannot use the "id" property at the Statement level (Data 2.4.4.3.s8.b3, XAPI-00070)', + config: [ + { + name: 'substatement invalid with property "id"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {id: 'fd41c918-b88b-4b20-a0a5-a4c32391aaa0'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00069, Data 2.4.4.3 when object is a statement + * A Sub-Statement cannot use the "stored" property. The LRS rejects with 400 Bad Request a Statement with a Sub-Statement where the “stored” property is present. + */ + name: 'A Sub-Statement cannot use the "stored" property (Data 2.4.4.3.s8.b3, XAPI-00069)', + config: [ + { + name: 'substatement invalid with property "stored"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {stored: '2013-05-18T05:32:34.804Z'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00068, Data 2.4.4.3 when object is a statement + * A Sub-Statement cannot use the "version" property. The LRS rejects with 400 Bad Request a Statement with a Sub-Statement where the “version” property is present. + */ + name: 'A Sub-Statement cannot use the "version" property (Data 2.4.4.3.s8.b3, XAPI-00068)', + config: [ + { + name: 'substatement invalid with property "version"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'}, + {version: '1.0.0'} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00067, Data 2.4.4.3 when object is a statement + * A Sub-Statement cannot use the "authority" property. The LRS rejects with 400 Bad Request a Statement with a Sub-Statement where the “authority” property is present. + */ + name: 'A Sub-Statement cannot use the "authority" property (Data 2.4.4.3.s8.b3, XAPI-00067)', + config: [ + { + name: 'substatement invalid with property "authority"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{statements.authority}}'}, + {authority: '{{agents.default}}'} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/timestamp_property.js b/test/v2_0/configs/timestamp_property.js new file mode 100644 index 00000000..0c8abc9e --- /dev/null +++ b/test/v2_0/configs/timestamp_property.js @@ -0,0 +1,80 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module, helper) { + "use strict"; + + // defines overwriting data + var INVALID_DATE = '01/011/2015'; + var FUTURE_DATE = new Date().setFullYear(new Date().getFullYear() + 5); + var INVALID_STRING = 'should fail'; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00022, Data 2.4 Statement Properties + * A "timestamp" property is a TimeStamp, per section 4.5. An LRS rejects with 400 Bad Request a statement if it has a TimeStamp and that TimeStamp is invalid. + */ + name: 'A "timestamp" property is a TimeStamp (Type, Data 2.4.7, Data 2.4.s1.table1.row7, XAPI-00022)', + config: [ + { + name: 'statement "template" invalid string', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement "template" invalid date', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_DATE} + ], + expect: [400] + }, + { + name: 'statement "template" future date', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: new Date(FUTURE_DATE).toISOString()} + ], + expect: [200] + }, + { + name: 'substatement "template" invalid string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{statements.default}}'}, + {timestamp: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'substatement "template" invalid date', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{statements.default}}'}, + {timestamp: INVALID_DATE} + ], + expect: [400] + }, + { + name: 'substatement "template" future date', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{statements.default}}'}, + {timestamp: new Date(FUTURE_DATE).toISOString()} + ], + expect: [200] + } + ] + }, + ]; + }; +}(module, require('./../../helper.js'))); diff --git a/test/v2_0/configs/timestamps.js b/test/v2_0/configs/timestamps.js new file mode 100644 index 00000000..716c530f --- /dev/null +++ b/test/v2_0/configs/timestamps.js @@ -0,0 +1,128 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module, helper) { + "use strict"; + + // defines overwriting data + var INVALID_DATE = '01/011/2015'; + var INVALID_STRING = 'should fail'; + var INVALID_DATE_00 = "2008-09-15T15:53:00.601-00"; + var INVALID_DATE_0000 = "2008-09-15T15:53:00.601-0000"; + var INVALID_DATE_00_00 = "2008-09-15T15:53:00.601-00:00"; + var VALID_RFC = "2008-09-15 15:53:00.601+00:00" + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00123, Data 4.5 ISO8601 Timestamps + * A Timestamp must conform to ISO 8601 Date format. An LRS rejects a statement with a Timestamp which doesn’t validate to ISO 8601 Extended or ISO 8601 Basic. + */ + name: 'A TimeStamp is defined as a Date/Time formatted according to ISO 8601 (Format, Data 4.5.s1.b1, ISO8601, XAPI-00123)', + config: [ + { + name: 'statement "template" invalid string in timestamp', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement "template" invalid date in timestamp', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_DATE} + ], + expect: [400] + }, + { + name: 'statement "template" invalid date in timestamp: did not reject statement timestamp with -00 offset', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_DATE_00} + ], + expect: [400] + }, + { + name: 'statement "template" invalid date in timestamp: did not reject statement timestamp with -0000 offset', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_DATE_0000} + ], + expect: [400] + }, + { + name: 'statement "template" invalid date in timestamp: did not reject statement timestamp with -00:00 offset', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_DATE_00_00} + ], + expect: [400] + }, + { + name: 'Statement "template" valid RFC 3339 date in timestamp', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: VALID_RFC} + ], + expect: [200] + }, + { + name: 'substatement "template" invalid string in timestamp', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'substatement "template" invalid date in timestamp', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_DATE} + ], + expect: [400] + }, + { + name: 'substatement "template" invalid date in timestamp: did not reject substatement timestamp with -00 offset', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_DATE_00} + ], + expect: [400] + }, + { + name: 'substatement "template" invalid date in timestamp: did not reject substatement timestamp with -0000 offset', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_DATE_0000} + ], + expect: [400] + }, + { + name: 'substatement "template" invalid date in timestamp: did not reject substatement timestamp with -00:00 offset', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: INVALID_DATE_00_00} + ], + expect: [400] + }, + { + name: 'Substatement "template" valid RFC 3339 date in timestamp', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: VALID_RFC} + ], + expect: [200] + }, + ] + } + ]; + }; +}(module, require('./../../helper.js'))); diff --git a/test/v2_0/configs/uuids.js b/test/v2_0/configs/uuids.js new file mode 100644 index 00000000..976f4832 --- /dev/null +++ b/test/v2_0/configs/uuids.js @@ -0,0 +1,184 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_NUMERIC = 12345; + var INVALID_OBJECT = {key: 'should fail'}; + var INVALID_UUID_TOO_MANY_DIGITS = 'AA97B177-9383-4934-8543-0F91A7A028368'; + var INVALID_UUID_INVALID_LETTER = 'MA97B177-9383-4934-8543-0F91A7A02836'; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00030, Data 2.4.1 Id + * All UUID types follow requirements of RFC4122. An LRS rejects with 400 Bad Request a statement which has a property which is required to be a UUID and does not follow RFC4122. + */ + /** XAPI-00027, Data 2.4.1 Id + * A Statement's "id" property is a UUID following RFC 4122. An LRS rejects with 400 Bad Request a statement which has an “id” and that “id” is invalid. + */ + name: 'All UUID types follow requirements of RFC4122 (Type, Data 2.4.1.s1, XAPI-00030, XAPI-00027)', + config: [ + { //XAPI-00027 + name: 'statement "id" invalid UUID with too many digits', + templates: [ + {statement: '{{statements.default}}'}, + {id: INVALID_UUID_TOO_MANY_DIGITS} + ], + expect: [400] + }, + { //XAPI-00027 + name: 'statement "id" invalid UUID with non A-F', + templates: [ + {statement: '{{statements.default}}'}, + {id: INVALID_UUID_INVALID_LETTER} + ], + expect: [400] + }, + { //XAPI-00027 + name: 'statement object statementref "id" invalid UUID with too many digits', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {object: {id: INVALID_UUID_TOO_MANY_DIGITS}} + ], + expect: [400] + }, + { //XAPI-00027 + name: 'statement object statementref "id" invalid UUID with non A-F', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'}, + {object: {id: INVALID_UUID_INVALID_LETTER}} + ], + expect: [400] + }, + { + name: 'statement context "registration" invalid UUID with too many digits', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {registration: INVALID_UUID_TOO_MANY_DIGITS} + ], + expect: [400] + }, + { + name: 'statement context "registration" invalid UUID with non A-F', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {registration: INVALID_UUID_INVALID_LETTER} + ], + expect: [400] + }, + { + name: 'statement context "statement" invalid UUID with too many digits', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {context: '{{contexts.default}}'}, + {statement: {id: INVALID_UUID_TOO_MANY_DIGITS}} + ], + expect: [400] + }, + { + name: 'statement substatement context "statement" invalid UUID with non A-F', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'}, + {context: '{{contexts.default}}'}, + {statement: {id: INVALID_UUID_INVALID_LETTER}} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00029, Data 2.4.1 Id + * All UUID types are in standard String form. An LRS rejects with 400 Bad Request a statement which has an property which is required to be a UUID and that property is not in standard string form + */ + /** XAPI-00028, Data 2.4.1 Id + * A Statement's "id" property is a String. An LRS rejects with 400 Bad Request a statement which has an “id” and that property is not a string + */ + name: 'All UUID types are in standard String form (Type, Data 2.4.1.s1, XAPI-00029, XAPI-00028)', + config: [ + { //XAPI-00028 + name: 'statement "id" invalid numeric', + templates: [ + {statement: '{{statements.default}}'}, + {id: INVALID_NUMERIC} + ], + expect: [400] + }, + { //XAPI-00028 + name: 'statement "id" invalid object', + templates: [ + {statement: '{{statements.default}}'}, + {id: INVALID_OBJECT} + ], + expect: [400] + }, + { //XAPI-00028 + name: 'statement object statementref "id" invalid numeric', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {object: {id: INVALID_NUMERIC}} + ], + expect: [400] + }, + { //XAPI-00028 + name: 'statement object statementref "id" invalid object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'}, + {object: {id: INVALID_OBJECT}} + ], + expect: [400] + }, + { + name: 'statement context "registration" invalid numeric', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {registration: INVALID_NUMERIC} + ], + expect: [400] + }, + { + name: 'statement context "registration" invalid object', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'}, + {registration: INVALID_OBJECT} + ], + expect: [400] + }, + { + name: 'statement context "statement" invalid numeric', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {context: '{{contexts.default}}'}, + {statement: {id: INVALID_NUMERIC}} + ], + expect: [400] + }, + { + name: 'statement substatement context "statement" invalid object', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'}, + {context: '{{contexts.default}}'}, + {statement: {id: INVALID_OBJECT}} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/verbs.js b/test/v2_0/configs/verbs.js new file mode 100644 index 00000000..50722a48 --- /dev/null +++ b/test/v2_0/configs/verbs.js @@ -0,0 +1,117 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_URI = 'ab=c://should.fail.com'; + var INVALID_LANGUAGE_MAP_NUMERIC = {'display': 12345}; + var INVALID_LANGUAGE_MAP_STRING = {'display': 'a12345 attended'}; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00044, Data 2.4.3 Verb + * A "verb" object contains an "id" property which is required to be an IRI. An LRS rejects with 400 Bad Request if a statement uses the Verb Object and “id” is absent or “id” is present, but the value is an invalid IRI. + * Covers this and next suite + */ + name: 'A "verb" property contains an "id" property (Multiplicity, Data 2.4.3.s3.table1.row1, XAPI-00044)', + config: [ + { + name: 'statement verb missing "id"', + templates: [ + {statement: '{{statements.verb}}'}, + {verb: '{{verbs.no_id}}'} + ], + expect: [400] + }, + { + name: 'statement substatement verb missing "id"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.verb}}'}, + {verb: '{{verbs.no_id}}'} + ], + expect: [400] + } + ] + }, + { //see above + name: 'A "verb" property\'s "id" property is an IRI (Type, Data 2.4.3.s3.table1.row1, XAPI-00044)', + config: [ + { + name: 'statement verb "id" not IRI', + templates: [ + {statement: '{{statements.verb}}'}, + {verb: '{{verbs.default}}'}, + {id: INVALID_URI} + ], + expect: [400] + }, + { + name: 'statement substatement verb "id" not IRI', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.verb}}'}, + {verb: INVALID_URI} + ], + expect: [400] + } + ] + }, + { + /** XAPI-00045, Data 2.4.3 Verb + * A "verb" property's "display" property is a Language Map. An LRS rejects with 400 Bad Request if a statement uses the Verb Object’s “display” property and it is not a valid Language Map. + * Further verification of language map in Data 2.2 Formatting + */ + name: 'A "verb" property\'s "display" property is a Language Map (Type, Data 2.4.3.s3.table1.row2, XAPI-00045)', + config: [ + { + name: 'statement verb "display" is numeric', + templates: [ + {statement: '{{statements.verb}}'}, + {verb: '{{verbs.default}}'}, + INVALID_LANGUAGE_MAP_NUMERIC + ], + expect: [400] + }, + { + name: 'statement verb "display" is string', + templates: [ + {statement: '{{statements.verb}}'}, + {verb: '{{verbs.default}}'}, + INVALID_LANGUAGE_MAP_STRING + ], + expect: [400] + }, + { + name: 'statement substatement verb "display" is numeric', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.verb}}'}, + {verb: '{{verbs.default}}'}, + INVALID_LANGUAGE_MAP_NUMERIC + ], + expect: [400] + }, + { + name: 'statement substatement verb "display" is string', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.verb}}'}, + {verb: '{{verbs.default}}'}, + INVALID_LANGUAGE_MAP_STRING + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/verify.js b/test/v2_0/configs/verify.js new file mode 100644 index 00000000..d91776e8 --- /dev/null +++ b/test/v2_0/configs/verify.js @@ -0,0 +1,951 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + +/** XAPI-00014, Data 2.2 Formatting Requirements + * All Objects are well-created JSON Objects (Nature of Binding) + * + * Note: All tests in this file pertain to this requirement. + */ + + // defines overwriting data + // var INVALID_STRING = 'should fail'; + // var INVALID_VERSION_0_9_9 = '0.9.9'; + // var INVALID_VERSION_1_1_0 = '1.1.0'; + // var VALID_VERSION_1_0 = '1.0'; + // var VALID_VERSION_1_0_9 = '1.0.9'; + var VALID_DESCRIPTION = { + 'description': { + 'en-GB': 'An example meeting that happened on a specific occasion with certain people present.', + 'en-US': 'An example meeting that happened on a specific occasion with certain people present.' + } + }; + var VALID_EXTENSIONS = { + extensions: { + 'http://example.com/profiles/meetings/extension/location': 'X:\\meetings\\minutes\\examplemeeting.one', + 'http://example.com/profiles/meetings/extension/reporter': { + 'name': 'Thomas', + 'id': 'http://openid.com/342' + } + } + }; + var VALID_INTERACTION_TYPE = { + 'interactionType': 'fill-in', + 'correctResponsesPattern': [ + 'Bob"s your uncle' + ] + }; + var VALID_MORE_INFO = {moreInfo: 'http://virtualmeeting.example.com/345256'}; + var VALID_NAME = { + 'name': { + 'en-GB': 'example meeting', + 'en-US': 'example meeting' + } + }; + var VALID_TYPE = {type: 'http://adlnet.gov/expapi/activities/meeting'}; + + // configures tests + module.exports.config = function () { + return [ + { // see above + name: 'Statements Verify Templates', + config: [ + { + name: 'should pass statement template', + templates: [ + {statement: '{{statements.default}}'}, + {timestamp: '2013-05-18T05:32:34.804Z'} + ], + expect: [200] + } + ] + }, + { // see above + name: 'Agents Verify Templates', + config: [ + { + name: 'should pass statement actor template', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement authority template', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement context instructor template', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement as agent template', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement"s agent template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement"s context instructor template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{agents.default}}'} + ], + expect: [200] + } + ] + }, + { // see above + name: 'Groups Verify Templates', + config: [ + { + name: 'should pass statement actor template', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement authority template', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.anonymous_two_member}}'} + ], + expect: [200] + }, + { + name: 'should pass statement context instructor template', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement context team template', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement as group template', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement"s group template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement"s context instructor template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement"s context team template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.default}}'} + ], + expect: [200] + } + ] + }, + { // see above + name: 'A Group is defined by "objectType" of an "actor" property or "object" property with value "Group" (Data 2.4.2.2.s2.table2.row1)', + config: [ + { + name: 'statement actor "objectType" accepts "Group"', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'statement authority "objectType" accepts "Group"', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.authority_group}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor "objectType" accepts "Group"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'statement context team "objectType" accepts "Group"', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group "objectType" accepts "Group"', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group "objectType" accepts "Group"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor "objectType" accepts "Group"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team "objectType" accepts "Group"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.default}}'} + ], + expect: [200] + } + ] + }, + { // see above + name: 'An Anonymous Group is defined by "objectType" of an "actor" or "object" with value "Group" and by none of "mbox", "mbox_sha1sum", "openid", or "account" being used (Data 2.4.2.2.s2.table1.row1)', + config: [ + { + name: 'statement actor does not require functional identifier', + templates: [ + {statement: '{{statements.actor}}'}, + {actor: '{{groups.anonymous}}'} + ], + expect: [200] + }, + { + name: 'statement authority does not require functional identifier', + templates: [ + {statement: '{{statements.authority}}'}, + {authority: '{{groups.authority_group}}'} + ], + expect: [200] + }, + { + name: 'statement context instructor does not require functional identifier', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.anonymous}}'} + ], + expect: [200] + }, + { + name: 'statement context team does not require functional identifier', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.anonymous}}'} + ], + expect: [200] + }, + { + name: 'statement substatement as group does not require functional identifier', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.anonymous}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s group does not require functional identifier', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.actor}}'}, + {actor: '{{groups.anonymous}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context instructor does not require functional identifier', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.instructor}}'}, + {instructor: '{{groups.anonymous}}'} + ], + expect: [200] + }, + { + name: 'statement substatement"s context team does not require functional identifier', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.team}}'}, + {team: '{{groups.anonymous}}'} + ], + expect: [200] + } + ] + }, + { //see above + name: 'Verbs Verify Templates', + config: [ + { + name: 'should pass statement verb template', + templates: [ + {statement: '{{statements.verb}}'}, + {verb: '{{verbs.default}}'} + ], + expect: [200] + }, + { + name: 'should pass substatement verb template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.verb}}'}, + {verb: '{{verbs.default}}'} + ], + expect: [200] + } + ] + }, + { //see above + name: 'Objects Verify Templates', + config: [ + { + name: 'should pass statement activity template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement activity template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement agent template', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement agent template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.agent}}'}, + {object: '{{agents.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement group template', + templates: [ + {statement: '{{statements.object_actor}}'}, + {object: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement group template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.group}}'}, + {object: '{{groups.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement StatementRef template', + templates: [ + {statement: '{{statements.object_statementref}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement StatementRef template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'} + ], + expect: [200] + }, + { + name: 'should pass statement SubStatement template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'} + ], + expect: [200] + } + ] + }, + { //see above + name: 'Activities Verify Templates', + config: [ + { + name: 'should pass statement activity default template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement activity default template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.default}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity choice template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.choice}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity fill-in template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.fill_in}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity numeric template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.numeric}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity likert template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.likert}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity long-fill-in template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.long_fill_in}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity matching template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.matching}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity other template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.other}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity performance template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.performance}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity sequencing template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.sequencing}}'} + ], + expect: [200] + }, + { + name: 'should pass statement activity true-false template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.true_false}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement activity choice template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.choice}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement activity likert template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.likert}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement activity matching template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.matching}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement activity performance template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.performance}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement activity sequencing template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.sequencing}}'} + ], + expect: [200] + } + ] + }, + { //see above + name: 'An Activity Definition uses the following properties: name, description, type, moreInfo, interactionType, or extensions (Format, Data 2.4.4.1.s2)', + config: [ + { + name: 'statement activity "definition" missing all properties', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {}} + ], + expect: [200] + }, + { + name: 'statement activity "definition" contains "name"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_NAME} + ], + expect: [200] + }, + { + name: 'statement activity "definition" contains "description"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_DESCRIPTION} + ], + expect: [200] + }, + { + name: 'statement activity "definition" contains "type"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_TYPE} + ], + expect: [200] + }, + { + name: 'statement activity "definition" contains "moreInfo"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_MORE_INFO} + ], + expect: [200] + }, + { + name: 'statement activity "definition" contains "extensions"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_EXTENSIONS} + ], + expect: [200] + }, + { + name: 'statement activity "definition" contains "interactionType"', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_INTERACTION_TYPE} + ], + expect: [200] + }, + { + name: 'statement substatement activity "definition" missing all properties', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: {}} + ], + expect: [200] + }, + { + name: 'statement substatement activity "definition" contains "name"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_NAME} + ], + expect: [200] + }, + { + name: 'statement substatement activity "definition" contains "description"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_DESCRIPTION} + ], + expect: [200] + }, + { + name: 'statement substatement activity "definition" contains "type"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_TYPE} + ], + expect: [200] + }, + { + name: 'statement substatement activity "definition" contains "moreInfo"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_MORE_INFO} + ], + expect: [200] + }, + { + name: 'statement substatement activity "definition" contains "extensions"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_EXTENSIONS} + ], + expect: [200] + }, + { + name: 'statement substatement activity "definition" contains "interactionType"', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_definition}}'}, + {definition: VALID_INTERACTION_TYPE} + ], + expect: [200] + } + ] + }, + { //see above + name: 'SubStatements Verify Templates', + config: [ + { + name: 'should pass statement SubStatement template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.default}}'} + ], + expect: [200] + } + ] + }, + { //see above + name: 'StatementRefs Verify Templates', + config: [ + { + name: 'should pass statement StatementRef template', + templates: [ + {statement: '{{statements.object_statementref}}'} + ], + expect: [200] + }, + { + name: 'should pass substatement StatementRef template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.statementref}}'} + ], + expect: [200] + } + ] + }, + { //see above + name: 'Results Verify Templates', + config: [ + { + name: 'should pass statement result template', + templates: [ + {statement: '{{statements.result}}'}, + {result: '{{results.default}}'} + ], + expect: [200] + }, + { + name: 'should pass substatement result template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.result}}'}, + {result: '{{results.default}}'} + ], + expect: [200] + } + ] + }, + { + name: 'Contexts Verify Templates', + config: [ + { + name: 'should pass statement context template', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.default}}'} + ], + expect: [200] + }, + { + name: 'should pass substatement context template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.default}}'} + ], + expect: [200] + } + ] + }, + { + name: 'A ContextActivity is defined as a single Activity of the "value" of the "contextActivities" property (definition, Data 2.4.6.2.s4.b2)', + config: [ + { + name: 'statement context "contextActivities parent" value is activity', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.parent}}'} + ], + expect: [200] + }, + { + name: 'statement context "contextActivities grouping" value is activity', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.grouping}}'} + ], + expect: [200] + }, + { + name: 'statement context "contextActivities category" value is activity', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.category}}'} + ], + expect: [200] + }, + { + name: 'statement context "contextActivities other" value is activity', + templates: [ + {statement: '{{statements.context}}'}, + {context: '{{contexts.other}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities parent" value is activity', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.parent}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities grouping" value is activity', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.grouping}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities category" value is activity', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.category}}'} + ], + expect: [200] + }, + { + name: 'statement substatement context "contextActivities other" value is activity', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.context}}'}, + {context: '{{contexts.other}}'} + ], + expect: [200] + } + ] + }, + { + name: 'Languages Verify Templates', + config: [ + { + name: 'should pass statement verb template', + templates: [ + {statement: '{{statements.verb}}'}, + {verb: '{{verbs.no_display}}'} + ], + expect: [200] + }, + { + name: 'should pass statement object template', + templates: [ + {statement: '{{statements.object_activity}}'}, + {object: '{{activities.no_languages}}'} + ], + expect: [200] + }, + { + name: 'should pass statement attachment template', + templates: [ + {statement: '{{statements.attachment}}'}, + { + attachments: [ + { + "usageType": "http://example.com/attachment-usage/test", + "display": {"en-US": "A test attachment"}, + "description": {"en-US": "A test attachment (description)"}, + "contentType": "text/plain; charset=ascii", + "length": 27, + "sha2": "495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a", + "fileUrl": "http://over.there.com/file.txt" + } + ] + } + ], + expect: [200] + }, + { + name: 'should pass statement substatement verb template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.verb}}'}, + {verb: '{{verbs.no_display}}'} + ], + expect: [200] + }, + { + name: 'should pass statement substatement object template', + templates: [ + {statement: '{{statements.object_substatement}}'}, + {object: '{{substatements.activity}}'}, + {object: '{{activities.no_languages}}'} + ], + expect: [200] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/version.js b/test/v2_0/configs/version.js new file mode 100644 index 00000000..b7f12896 --- /dev/null +++ b/test/v2_0/configs/version.js @@ -0,0 +1,71 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_STRING = 'should fail'; + var INVALID_VERSION_0_9_9 = '0.9.9'; + var INVALID_VERSION_1_1_0 = '1.1.0'; + var VALID_VERSION_1_0 = '1.0'; + var VALID_VERSION_1_0_9 = '1.0.9'; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00101, Data 2.4.10 Version + * An LRS rejects with error code 400 Bad Request, a Request which uses "version" and has the value set to anything but "1.0" or "1.0.x", where x is the semantic versioning number + */ + name: 'An LRS rejects with error code 400 Bad Request, a Request which uses "version" and has the value set to anything but "1.0" or "1.0.x", where x is the semantic versioning number (Format, Data 2.4.10.s2.b1, Data 2.4.10.s3.b1, Communication 3.3.s3.b3, Communication 3.3.s3.b6, XAPI-00101)', + config: [ + { + name: 'statement "version" valid 1.0', + templates: [ + {statement: '{{statements.default}}'}, + {version: VALID_VERSION_1_0} + ], + expect: [200] + }, + { + name: 'statement "version" valid 1.0.9', + templates: [ + {statement: '{{statements.default}}'}, + {version: VALID_VERSION_1_0_9} + ], + expect: [200] + }, + { + name: 'statement "version" invalid string', + templates: [ + {statement: '{{statements.default}}'}, + {version: INVALID_STRING} + ], + expect: [400] + }, + { + name: 'statement "version" invalid 0.9.9', + templates: [ + {statement: '{{statements.default}}'}, + {version: INVALID_VERSION_0_9_9} + ], + expect: [400] + }, + { + name: 'statement "version" invalid 1.1.0', + templates: [ + {statement: '{{statements.default}}'}, + {version: INVALID_VERSION_1_1_0} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/configs/voiding.js b/test/v2_0/configs/voiding.js new file mode 100644 index 00000000..c9c9bf8e --- /dev/null +++ b/test/v2_0/configs/voiding.js @@ -0,0 +1,63 @@ +/** + * Description : This is a test suite that tests an LRS endpoint based on the testing requirements document + * found at https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + * https://github.com/adlnet/xAPI_LRS_Test/blob/master/TestingRequirements.md + * + */ +(function (module) { + "use strict"; + + // defines overwriting data + var INVALID_URI = 'ab=c://should.fail.com'; + var INVALID_LANGUAGE_MAP = {'display': { 'a12345': 'attended'}}; + + // configures tests + module.exports.config = function () { + return [ + { + /** XAPI-00019, 2.3.2 Voiding + * A Voiding Statement is defined as a Statement whose "verb" property's "id" property's IRI ending with "voided" + */ + name: 'A Voiding Statement is defined as a Statement whose "verb" property\'s "id" property\'s IRI ending with "voided" (Data 2.3.2, XAPI-00019)', + config: [ + { + name: 'statement verb voided IRI ends with "voided" (WARNING: this applies "Upon receiving a Statement that voids another, the LRS SHOULD NOT* reject the request on the grounds of the Object of that voiding Statement not being present")', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {verb: '{{verbs.voided}}'} + ], + expect: [200] + } + ] + }, + { + /** XAPI-00017, Data 2.3.2 Voiding + * An LRS rejects a Voiding Statement with 400 Bad Request if the "objectType" field does not have a value of "StatementRef" + */ + /** XAPI-00020, 2.3.2 Voiding + * A Voiding Statement's "objectType" field has a value of "StatementRef" + */ + name: 'A Voiding Statement\'s "objectType" field has a value of "StatementRef" (Format, Data 2.3.2.s2.b1, XAPI-00017, XAPI-00020)', + config: [ + { //XAPI-00020 + name: 'statement verb voided uses substatement with "StatementRef"', + templates: [ + {statement: '{{statements.object_statementref}}'}, + {verb: '{{verbs.voided}}'} + ], + expect: [200] + }, + { //XAPI-00017 + name: 'statement verb voided does not use object "StatementRef"', + templates: [ + {statement: '{{statements.verb}}'}, + {verb: '{{verbs.voided}}'} + ], + expect: [400] + } + ] + } + ]; + }; +}(module)); diff --git a/test/v2_0/templates/activities/choice.json b/test/v2_0/templates/activities/choice.json new file mode 100644 index 00000000..e720de2a --- /dev/null +++ b/test/v2_0/templates/activities/choice.json @@ -0,0 +1,51 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Choice" + }, + "description": { + "en": "Which of these prototypes are available at the beta site?" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "choice", + "correctResponsesPattern": [ + "golf[,]tetris" + ], + "choices": [ + { + "id": "golf", + "description": { + "en-US": "Golf Example" + } + }, + { + "id": "facebook", + "description": { + "en-US": "Facebook App" + } + }, + { + "id": "tetris", + "description": { + "en-US": "Tetris Example" + } + }, + { + "id": "scrabble", + "description": { + "en-US": "Scrabble Example" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/default.json b/test/v2_0/templates/activities/default.json new file mode 100644 index 00000000..b72e14da --- /dev/null +++ b/test/v2_0/templates/activities/default.json @@ -0,0 +1,23 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/description_invalid.json b/test/v2_0/templates/activities/description_invalid.json new file mode 100644 index 00000000..5502f76d --- /dev/null +++ b/test/v2_0/templates/activities/description_invalid.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting", + "de": "Beispiel Treffen" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present.", + "something": "اجتماع سبيل المثال ما حدث في مناسبة محددة مع بعض الحاضرين." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/fill_in.json b/test/v2_0/templates/activities/fill_in.json new file mode 100644 index 00000000..f8407fe3 --- /dev/null +++ b/test/v2_0/templates/activities/fill_in.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Fill-In" + }, + "description": { + "en": "Ben is often heard saying:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "fill-in", + "correctResponsesPattern": [ + "Bob's your uncle" + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/interaction_type_invalid_iri.json b/test/v2_0/templates/activities/interaction_type_invalid_iri.json new file mode 100644 index 00000000..98c0818b --- /dev/null +++ b/test/v2_0/templates/activities/interaction_type_invalid_iri.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Fill-In" + }, + "description": { + "en": "Ben is often heard saying:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "ab=c://should.fail.com", + "correctResponsesPattern": [ + "Bob's your uncle" + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/interaction_type_invalid_numeric.json b/test/v2_0/templates/activities/interaction_type_invalid_numeric.json new file mode 100644 index 00000000..e07eb758 --- /dev/null +++ b/test/v2_0/templates/activities/interaction_type_invalid_numeric.json @@ -0,0 +1,51 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Likert" + }, + "description": { + "en": "How awesome is Experience API?" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": 12345, + "correctResponsesPattern": [ + "likert_3" + ], + "scale": [ + { + "id": "likert_0", + "description": { + "en-US": "It's OK" + } + }, + { + "id": "likert_1", + "description": { + "en-US": "It's Pretty Cool" + } + }, + { + "id": "likert_2", + "description": { + "en-US": "It's Damn Cool" + } + }, + { + "id": "likert_3", + "description": { + "en-US": "It's Gonna Change the World" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/interaction_type_invalid_object.json b/test/v2_0/templates/activities/interaction_type_invalid_object.json new file mode 100644 index 00000000..74976607 --- /dev/null +++ b/test/v2_0/templates/activities/interaction_type_invalid_object.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Long-Fill-In" + }, + "description": { + "en": "What is the purpose of the xAPI?" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": {"test": "value"}, + "correctResponsesPattern": [ + "{case_matters=false}{lang=en}To store and provide access to learning experiences." + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/interaction_type_invalid_string.json b/test/v2_0/templates/activities/interaction_type_invalid_string.json new file mode 100644 index 00000000..9d4c9f10 --- /dev/null +++ b/test/v2_0/templates/activities/interaction_type_invalid_string.json @@ -0,0 +1,77 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Matching" + }, + "description": { + "en": "Match these people to their kickball team:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "should error", + "correctResponsesPattern": [ + "ben[.]3[,]chris[.]2[,]troy[.]4[,]freddie[.]1" + ], + "source": [ + { + "id": "ben", + "description": { + "en-US": "Ben" + } + }, + { + "id": "chris", + "description": { + "en-US": "Chris" + } + }, + { + "id": "troy", + "description": { + "en-US": "Troy" + } + }, + { + "id": "freddie", + "description": { + "en-US": "Freddie" + } + } + ], + "target": [ + { + "id": "1", + "description": { + "en-US": "Swift Kick in the Grass" + } + }, + { + "id": "2", + "description": { + "en-US": "We got Runs" + } + }, + { + "id": "3", + "description": { + "en-US": "Duck" + } + }, + { + "id": "4", + "description": { + "en-US": "Van Delay Industries" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/language_maps_valid_description.json b/test/v2_0/templates/activities/language_maps_valid_description.json new file mode 100644 index 00000000..cb99eb63 --- /dev/null +++ b/test/v2_0/templates/activities/language_maps_valid_description.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting", + "de-DE": "Beispiel Treffen" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present.", + "zh-Hant": "所發生的與目前某些人特定場合的一個例子會議。" + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/language_maps_valid_name.json b/test/v2_0/templates/activities/language_maps_valid_name.json new file mode 100644 index 00000000..8aeff623 --- /dev/null +++ b/test/v2_0/templates/activities/language_maps_valid_name.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting", + "de-DE": "Beispiel Treffen" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present.", + "de-DE": "Ein Beispiel Sitzung, die auf einem bestimmten Anlass mit bestimmten Personen zufällig anwesend." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/likert.json b/test/v2_0/templates/activities/likert.json new file mode 100644 index 00000000..9c0122df --- /dev/null +++ b/test/v2_0/templates/activities/likert.json @@ -0,0 +1,51 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Likert" + }, + "description": { + "en": "How awesome is Experience API?" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "likert", + "correctResponsesPattern": [ + "likert_3" + ], + "scale": [ + { + "id": "likert_0", + "description": { + "en-US": "It's OK" + } + }, + { + "id": "likert_1", + "description": { + "en-US": "It's Pretty Cool" + } + }, + { + "id": "likert_2", + "description": { + "en-US": "It's Damn Cool" + } + }, + { + "id": "likert_3", + "description": { + "en-US": "It's Gonna Change the World" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/likert_valid_language_map.json b/test/v2_0/templates/activities/likert_valid_language_map.json new file mode 100644 index 00000000..bb72f828 --- /dev/null +++ b/test/v2_0/templates/activities/likert_valid_language_map.json @@ -0,0 +1,52 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Likert" + }, + "description": { + "en": "How awesome is Experience API?" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "likert", + "correctResponsesPattern": [ + "likert_3" + ], + "scale": [ + { + "id": "likert_0", + "description": { + "en-US": "It's OK", + "sr-Latn-RS": "U redu je" + } + }, + { + "id": "likert_1", + "description": { + "en-US": "It's Pretty Cool" + } + }, + { + "id": "likert_2", + "description": { + "en-US": "It's Damn Cool" + } + }, + { + "id": "likert_3", + "description": { + "en-US": "It's Gonna Change the World" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/long_fill_in.json b/test/v2_0/templates/activities/long_fill_in.json new file mode 100644 index 00000000..c2f40abe --- /dev/null +++ b/test/v2_0/templates/activities/long_fill_in.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Long-Fill-In" + }, + "description": { + "en": "What is the purpose of the xAPI?" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "long-fill-in", + "correctResponsesPattern": [ + "{case_matters=false}{lang=en}To store and provide access to learning experiences." + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/matching.json b/test/v2_0/templates/activities/matching.json new file mode 100644 index 00000000..ba0fa322 --- /dev/null +++ b/test/v2_0/templates/activities/matching.json @@ -0,0 +1,77 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Matching" + }, + "description": { + "en": "Match these people to their kickball team:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "matching", + "correctResponsesPattern": [ + "ben[.]3[,]chris[.]2[,]troy[.]4[,]freddie[.]1" + ], + "source": [ + { + "id": "ben", + "description": { + "en-US": "Ben" + } + }, + { + "id": "chris", + "description": { + "en-US": "Chris" + } + }, + { + "id": "troy", + "description": { + "en-US": "Troy" + } + }, + { + "id": "freddie", + "description": { + "en-US": "Freddie" + } + } + ], + "target": [ + { + "id": "1", + "description": { + "en-US": "Swift Kick in the Grass" + } + }, + { + "id": "2", + "description": { + "en-US": "We got Runs" + } + }, + { + "id": "3", + "description": { + "en-US": "Duck" + } + }, + { + "id": "4", + "description": { + "en-US": "Van Delay Industries" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/matching_language_map.json b/test/v2_0/templates/activities/matching_language_map.json new file mode 100644 index 00000000..2c8fee49 --- /dev/null +++ b/test/v2_0/templates/activities/matching_language_map.json @@ -0,0 +1,78 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Matching" + }, + "description": { + "en": "Match these people to their kickball team:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "matching", + "correctResponsesPattern": [ + "ben[.]3[,]chris[.]2[,]troy[.]4[,]freddie[.]1" + ], + "source": [ + { + "id": "ben", + "description": { + "en-US": "Ben" + } + }, + { + "id": "chris", + "description": { + "en-US": "Chris" + } + }, + { + "id": "troy", + "description": { + "en-US": "Troy" + } + }, + { + "id": "freddie", + "description": { + "en-US": "Freddie" + } + } + ], + "target": [ + { + "id": "1", + "description": { + "en-US": "Swift Kick in the Grass", + "ja": "草でスイフトキック" + } + }, + { + "id": "2", + "description": { + "en-US": "We got Runs" + } + }, + { + "id": "3", + "description": { + "en-US": "Duck" + } + }, + { + "id": "4", + "description": { + "en-US": "Van Delay Industries" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/matching_source.json b/test/v2_0/templates/activities/matching_source.json new file mode 100644 index 00000000..929ddeb8 --- /dev/null +++ b/test/v2_0/templates/activities/matching_source.json @@ -0,0 +1,51 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Matching" + }, + "description": { + "en": "Match these people to their kickball team:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "matching", + "correctResponsesPattern": [ + "ben[.]3[,]chris[.]2[,]troy[.]4[,]freddie[.]1" + ], + "source": [ + { + "id": "ben", + "description": { + "en-US": "Ben" + } + }, + { + "id": "chris", + "description": { + "en-US": "Chris" + } + }, + { + "id": "troy", + "description": { + "en-US": "Troy" + } + }, + { + "id": "freddie", + "description": { + "en-US": "Freddie" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/matching_target.json b/test/v2_0/templates/activities/matching_target.json new file mode 100644 index 00000000..909e9d3a --- /dev/null +++ b/test/v2_0/templates/activities/matching_target.json @@ -0,0 +1,51 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Matching" + }, + "description": { + "en": "Match these people to their kickball team:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "matching", + "correctResponsesPattern": [ + "ben[.]3[,]chris[.]2[,]troy[.]4[,]freddie[.]1" + ], + "target": [ + { + "id": "1", + "description": { + "en-US": "Swift Kick in the Grass" + } + }, + { + "id": "2", + "description": { + "en-US": "We got Runs" + } + }, + { + "id": "3", + "description": { + "en-US": "Duck" + } + }, + { + "id": "4", + "description": { + "en-US": "Van Delay Industries" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/name_invalid.json b/test/v2_0/templates/activities/name_invalid.json new file mode 100644 index 00000000..988f2df2 --- /dev/null +++ b/test/v2_0/templates/activities/name_invalid.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting", + "something": "Beispiel Treffen" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present.", + "de": "Ein Beispiel Sitzung, die auf einem bestimmten Anlass mit bestimmten Personen zufällig anwesend." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/no_definition.json b/test/v2_0/templates/activities/no_definition.json new file mode 100644 index 00000000..fb6b1377 --- /dev/null +++ b/test/v2_0/templates/activities/no_definition.json @@ -0,0 +1,4 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/no_extensions.json b/test/v2_0/templates/activities/no_extensions.json new file mode 100644 index 00000000..aeccc6c9 --- /dev/null +++ b/test/v2_0/templates/activities/no_extensions.json @@ -0,0 +1,16 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/no_id.json b/test/v2_0/templates/activities/no_id.json new file mode 100644 index 00000000..0fdc0b9d --- /dev/null +++ b/test/v2_0/templates/activities/no_id.json @@ -0,0 +1,13 @@ +{ + "objectType": "Activity", + "definition": { + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/no_languages.json b/test/v2_0/templates/activities/no_languages.json new file mode 100644 index 00000000..89630d8c --- /dev/null +++ b/test/v2_0/templates/activities/no_languages.json @@ -0,0 +1,14 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/no_objectType.json b/test/v2_0/templates/activities/no_objectType.json new file mode 100644 index 00000000..dec0a16f --- /dev/null +++ b/test/v2_0/templates/activities/no_objectType.json @@ -0,0 +1,15 @@ +{ + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256" + } +} diff --git a/test/v2_0/templates/activities/numeric.json b/test/v2_0/templates/activities/numeric.json new file mode 100644 index 00000000..1dc1bfa8 --- /dev/null +++ b/test/v2_0/templates/activities/numeric.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Numeric" + }, + "description": { + "en": "How many jokes is Chris the butt of each day?" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "numeric", + "correctResponsesPattern": [ + "4[:]" + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/numeric_description.json b/test/v2_0/templates/activities/numeric_description.json new file mode 100644 index 00000000..645a10ad --- /dev/null +++ b/test/v2_0/templates/activities/numeric_description.json @@ -0,0 +1,20 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": 1234, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/numeric_name.json b/test/v2_0/templates/activities/numeric_name.json new file mode 100644 index 00000000..5276d2d9 --- /dev/null +++ b/test/v2_0/templates/activities/numeric_name.json @@ -0,0 +1,20 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": 1234, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/other.json b/test/v2_0/templates/activities/other.json new file mode 100644 index 00000000..9f06b1d6 --- /dev/null +++ b/test/v2_0/templates/activities/other.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Other" + }, + "description": { + "en": "On this map, please mark Franklin, TN" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "other", + "correctResponsesPattern": [ + "(35.937432,-86.868896)" + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/performance.json b/test/v2_0/templates/activities/performance.json new file mode 100644 index 00000000..2b3bf399 --- /dev/null +++ b/test/v2_0/templates/activities/performance.json @@ -0,0 +1,45 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Performance" + }, + "description": { + "en": "This interaction measures performance over a day of RS sports:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "performance", + "correctResponsesPattern": [ + "pong[.]1:[,]dg[.]:10[,]lunch[.]" + ], + "steps": [ + { + "id": "pong", + "description": { + "en-US": "Net pong matches won" + } + }, + { + "id": "dg", + "description": { + "en-US": "Strokes over par in disc golf at Liberty" + } + }, + { + "id": "lunch", + "description": { + "en-US": "Lunch having been eaten" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/sequencing.json b/test/v2_0/templates/activities/sequencing.json new file mode 100644 index 00000000..1ae137cd --- /dev/null +++ b/test/v2_0/templates/activities/sequencing.json @@ -0,0 +1,51 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Sequencing" + }, + "description": { + "en": "Order players by their pong ladder position:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "sequencing", + "correctResponsesPattern": [ + "tim[,]mike[,]ells[,]ben" + ], + "choices": [ + { + "id": "tim", + "description": { + "en-US": "Tim" + } + }, + { + "id": "ben", + "description": { + "en-US": "Ben" + } + }, + { + "id": "ells", + "description": { + "en-US": "Ells" + } + }, + { + "id": "mike", + "description": { + "en-US": "Mike" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/activities/string_description.json b/test/v2_0/templates/activities/string_description.json new file mode 100644 index 00000000..9ec54d0d --- /dev/null +++ b/test/v2_0/templates/activities/string_description.json @@ -0,0 +1,20 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": "this is not a language map", + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/string_name.json b/test/v2_0/templates/activities/string_name.json new file mode 100644 index 00000000..dc5dd2f8 --- /dev/null +++ b/test/v2_0/templates/activities/string_name.json @@ -0,0 +1,20 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": "this is not a language map", + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} diff --git a/test/v2_0/templates/activities/true_false.json b/test/v2_0/templates/activities/true_false.json new file mode 100644 index 00000000..a7b9aa40 --- /dev/null +++ b/test/v2_0/templates/activities/true_false.json @@ -0,0 +1,25 @@ +{ + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "True-False" + }, + "description": { + "en": "Does the xAPI include the concept of statements?" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "true-false", + "correctResponsesPattern": [ + "true" + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/agents/account.json b/test/v2_0/templates/agents/account.json new file mode 100644 index 00000000..78258cc1 --- /dev/null +++ b/test/v2_0/templates/agents/account.json @@ -0,0 +1,8 @@ +{ + "objectType": "Agent", + "name": "xAPI account", + "account": { + "homePage": "http://www.example.com", + "name": "xAPI account name" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/agents/account_no_homepage.json b/test/v2_0/templates/agents/account_no_homepage.json new file mode 100644 index 00000000..6cc6ba98 --- /dev/null +++ b/test/v2_0/templates/agents/account_no_homepage.json @@ -0,0 +1,7 @@ +{ + "objectType": "Agent", + "name": "xAPI account", + "account": { + "name": "xAPI account name" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/agents/account_no_name.json b/test/v2_0/templates/agents/account_no_name.json new file mode 100644 index 00000000..0e500438 --- /dev/null +++ b/test/v2_0/templates/agents/account_no_name.json @@ -0,0 +1,7 @@ +{ + "objectType": "Agent", + "name": "xAPI account", + "account": { + "homePage": "http://www.example.com" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/agents/default.json b/test/v2_0/templates/agents/default.json new file mode 100644 index 00000000..ef4dc4e0 --- /dev/null +++ b/test/v2_0/templates/agents/default.json @@ -0,0 +1,5 @@ +{ + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" +} \ No newline at end of file diff --git a/test/v2_0/templates/agents/mbox.json b/test/v2_0/templates/agents/mbox.json new file mode 100644 index 00000000..ef4dc4e0 --- /dev/null +++ b/test/v2_0/templates/agents/mbox.json @@ -0,0 +1,5 @@ +{ + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" +} \ No newline at end of file diff --git a/test/v2_0/templates/agents/mbox_sha1sum.json b/test/v2_0/templates/agents/mbox_sha1sum.json new file mode 100644 index 00000000..b5d1df89 --- /dev/null +++ b/test/v2_0/templates/agents/mbox_sha1sum.json @@ -0,0 +1,5 @@ +{ + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" +} \ No newline at end of file diff --git a/test/v2_0/templates/agents/openid.json b/test/v2_0/templates/agents/openid.json new file mode 100644 index 00000000..07c9ad18 --- /dev/null +++ b/test/v2_0/templates/agents/openid.json @@ -0,0 +1,5 @@ +{ + "objectType": "Agent", + "name": "xAPI openid", + "openid": "http://openid.example.org/12345" +} \ No newline at end of file diff --git a/test/v2_0/templates/attachments/.gitattributes b/test/v2_0/templates/attachments/.gitattributes new file mode 100644 index 00000000..3df8d630 --- /dev/null +++ b/test/v2_0/templates/attachments/.gitattributes @@ -0,0 +1 @@ +*.part text eol=crlf \ No newline at end of file diff --git a/test/v2_0/templates/attachments/basic_image_p2.jpeg b/test/v2_0/templates/attachments/basic_image_p2.jpeg new file mode 100644 index 00000000..49432887 Binary files /dev/null and b/test/v2_0/templates/attachments/basic_image_p2.jpeg differ diff --git a/test/v2_0/templates/attachments/simple_text1.txt b/test/v2_0/templates/attachments/simple_text1.txt new file mode 100644 index 00000000..eecb3871 --- /dev/null +++ b/test/v2_0/templates/attachments/simple_text1.txt @@ -0,0 +1 @@ +here is a simple attachment \ No newline at end of file diff --git a/test/v2_0/templates/attachments/simple_text2.txt b/test/v2_0/templates/attachments/simple_text2.txt new file mode 100644 index 00000000..5f09d310 --- /dev/null +++ b/test/v2_0/templates/attachments/simple_text2.txt @@ -0,0 +1 @@ +here is another simple attachment \ No newline at end of file diff --git a/test/v2_0/templates/attachments/simple_text3.txt b/test/v2_0/templates/attachments/simple_text3.txt new file mode 100644 index 00000000..cb9577be --- /dev/null +++ b/test/v2_0/templates/attachments/simple_text3.txt @@ -0,0 +1 @@ +here is a third attachment \ No newline at end of file diff --git a/test/v2_0/templates/contexts/all_activities.json b/test/v2_0/templates/contexts/all_activities.json new file mode 100644 index 00000000..8d3dad6c --- /dev/null +++ b/test/v2_0/templates/contexts/all_activities.json @@ -0,0 +1,99 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "contextActivities": { + "other": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + }, + "category": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + }, + "grouping": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + }, + "parent": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } + } +} diff --git a/test/v2_0/templates/contexts/category.json b/test/v2_0/templates/contexts/category.json new file mode 100644 index 00000000..e03a5b0e --- /dev/null +++ b/test/v2_0/templates/contexts/category.json @@ -0,0 +1,33 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "contextActivities": { + "category": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/context_agents.json b/test/v2_0/templates/contexts/context_agents.json new file mode 100644 index 00000000..8693ff44 --- /dev/null +++ b/test/v2_0/templates/contexts/context_agents.json @@ -0,0 +1,15 @@ +{ + "contextAgents": [ + { + "objectType": "contextAgent", + "agent": { + "objectType": "Agent", + "mbox": "mailto:player-1@example.com" + }, + "relevantTypes": [ + "https://example.com/xapi/american-footbal/activity-types/personnel/player", + "https://example.com/xapi/american-footbal/activity-types/position/quarterback" + ] + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/context_groups.json b/test/v2_0/templates/contexts/context_groups.json new file mode 100644 index 00000000..e2f90a04 --- /dev/null +++ b/test/v2_0/templates/contexts/context_groups.json @@ -0,0 +1,40 @@ +{ + "contextGroups": [ + { + "objectType": "contextGroup", + "group": { + "objectType": "Group", + "mbox": "mailto:team-1@example.com", + "member": [ + { + "objectType": "Agent", + "mbox": "mailto:player-1@example.com" + }, + { + "objectType": "Agent", + "mbox": "mailto:player-2@example.com" + } + ] + }, + "relevantTypes": [ + "https://example.com/xapi/american-footbal/activity-types/team/a" + ] + }, + { + "objectType": "contextGroup", + "group": { + "objectType": "Group", + "mbox": "mailto:team-2@example.com", + "member": [ + { + "objectType": "Agent", + "mbox": "mailto:player-3@example.com" + } + ] + }, + "relevantTypes": [ + "https://example.com/xapi/american-footbal/activity-types/team/b" + ] + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/default.json b/test/v2_0/templates/contexts/default.json new file mode 100644 index 00000000..984fb5ce --- /dev/null +++ b/test/v2_0/templates/contexts/default.json @@ -0,0 +1,17 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "revision": "rev_10_3_2", + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "extensions": { + "http://example.com/profiles/meetings/contextextensions/airspeed": "600mph", + "http://example.com/profiles/meetings/contextextensions/pilot": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/grouping.json b/test/v2_0/templates/contexts/grouping.json new file mode 100644 index 00000000..e1718abd --- /dev/null +++ b/test/v2_0/templates/contexts/grouping.json @@ -0,0 +1,33 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "contextActivities": { + "grouping": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/instructor.json b/test/v2_0/templates/contexts/instructor.json new file mode 100644 index 00000000..31878343 --- /dev/null +++ b/test/v2_0/templates/contexts/instructor.json @@ -0,0 +1,12 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "instructor": { + "objectType": "Agent" + }, + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/invalid_activity.json b/test/v2_0/templates/contexts/invalid_activity.json new file mode 100644 index 00000000..60ff07dc --- /dev/null +++ b/test/v2_0/templates/contexts/invalid_activity.json @@ -0,0 +1,33 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "contextActivities": { + "consequences": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } + } +} diff --git a/test/v2_0/templates/contexts/no_extensions.json b/test/v2_0/templates/contexts/no_extensions.json new file mode 100644 index 00000000..87705b7e --- /dev/null +++ b/test/v2_0/templates/contexts/no_extensions.json @@ -0,0 +1,9 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/no_platform.json b/test/v2_0/templates/contexts/no_platform.json new file mode 100644 index 00000000..8b26f526 --- /dev/null +++ b/test/v2_0/templates/contexts/no_platform.json @@ -0,0 +1,16 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "revision": "rev_10_3_2", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "extensions": { + "http://example.com/profiles/meetings/contextextensions/airspeed": "600mph", + "http://example.com/profiles/meetings/contextextensions/pilot": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/no_revision.json b/test/v2_0/templates/contexts/no_revision.json new file mode 100644 index 00000000..06e59110 --- /dev/null +++ b/test/v2_0/templates/contexts/no_revision.json @@ -0,0 +1,16 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "extensions": { + "http://example.com/profiles/meetings/contextextensions/airspeed": "600mph", + "http://example.com/profiles/meetings/contextextensions/pilot": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/other.json b/test/v2_0/templates/contexts/other.json new file mode 100644 index 00000000..8c245ef7 --- /dev/null +++ b/test/v2_0/templates/contexts/other.json @@ -0,0 +1,33 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "contextActivities": { + "other": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/parent.json b/test/v2_0/templates/contexts/parent.json new file mode 100644 index 00000000..59690a5f --- /dev/null +++ b/test/v2_0/templates/contexts/parent.json @@ -0,0 +1,33 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "contextActivities": { + "parent": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/contexts/team.json b/test/v2_0/templates/contexts/team.json new file mode 100644 index 00000000..11012abb --- /dev/null +++ b/test/v2_0/templates/contexts/team.json @@ -0,0 +1,12 @@ +{ + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "team": { + "objectType": "Group" + }, + "platform": "Example virtual meeting software", + "language": "tlh", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/anonymous.json b/test/v2_0/templates/groups/anonymous.json new file mode 100644 index 00000000..35c9be26 --- /dev/null +++ b/test/v2_0/templates/groups/anonymous.json @@ -0,0 +1,11 @@ +{ + "objectType": "Group", + "name": "Group Anonymous", + "member": [ + { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/anonymous_no_member.json b/test/v2_0/templates/groups/anonymous_no_member.json new file mode 100644 index 00000000..4ed00123 --- /dev/null +++ b/test/v2_0/templates/groups/anonymous_no_member.json @@ -0,0 +1,4 @@ +{ + "objectType": "Group", + "name": "Group Anonymous" +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/anonymous_two_member.json b/test/v2_0/templates/groups/anonymous_two_member.json new file mode 100644 index 00000000..1665f9ea --- /dev/null +++ b/test/v2_0/templates/groups/anonymous_two_member.json @@ -0,0 +1,14 @@ +{ + "objectType": "Group", + "member": [ + { + "account": { + "homePage": "http://example.com/xAPI/OAuth/Token", + "name": "oauth_consumer_x75db" + } + }, + { + "mbox": "mailto:bob@example.com" + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/authority_group.json b/test/v2_0/templates/groups/authority_group.json new file mode 100644 index 00000000..db2ce48c --- /dev/null +++ b/test/v2_0/templates/groups/authority_group.json @@ -0,0 +1,14 @@ +{ + "objectType": "Group", + "member": [ + { + "account": { + "homePage": "http://example.com/xAPI/OAuth/Token", + "name": "oauth_consumer_x75db" + } + }, + { + "mbox": "mailto:bob_authority@example.com" + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/default.json b/test/v2_0/templates/groups/default.json new file mode 100644 index 00000000..28270a30 --- /dev/null +++ b/test/v2_0/templates/groups/default.json @@ -0,0 +1,5 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "mbox": "mailto:xapi@adlnet.gov" +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_account.json b/test/v2_0/templates/groups/identified_account.json new file mode 100644 index 00000000..bf3eb70e --- /dev/null +++ b/test/v2_0/templates/groups/identified_account.json @@ -0,0 +1,15 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "account": { + "homePage": "http://www.example.com", + "name": "xAPI account name" + }, + "member": [ + { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_account_no_homepage.json b/test/v2_0/templates/groups/identified_account_no_homepage.json new file mode 100644 index 00000000..020854fd --- /dev/null +++ b/test/v2_0/templates/groups/identified_account_no_homepage.json @@ -0,0 +1,14 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "account": { + "name": "xAPI account name" + }, + "member": [ + { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_account_no_member.json b/test/v2_0/templates/groups/identified_account_no_member.json new file mode 100644 index 00000000..b21f7430 --- /dev/null +++ b/test/v2_0/templates/groups/identified_account_no_member.json @@ -0,0 +1,8 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "account": { + "homePage": "http://www.example.com", + "name": "xAPI account name" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_account_no_name.json b/test/v2_0/templates/groups/identified_account_no_name.json new file mode 100644 index 00000000..fa2c5820 --- /dev/null +++ b/test/v2_0/templates/groups/identified_account_no_name.json @@ -0,0 +1,14 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "account": { + "homePage": "http://www.example.com" + }, + "member": [ + { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_mbox.json b/test/v2_0/templates/groups/identified_mbox.json new file mode 100644 index 00000000..c82e2ce1 --- /dev/null +++ b/test/v2_0/templates/groups/identified_mbox.json @@ -0,0 +1,12 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "mbox": "mailto:xapi@adlnet.gov", + "member": [ + { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_mbox_no_member.json b/test/v2_0/templates/groups/identified_mbox_no_member.json new file mode 100644 index 00000000..28270a30 --- /dev/null +++ b/test/v2_0/templates/groups/identified_mbox_no_member.json @@ -0,0 +1,5 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "mbox": "mailto:xapi@adlnet.gov" +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_mbox_sha1sum.json b/test/v2_0/templates/groups/identified_mbox_sha1sum.json new file mode 100644 index 00000000..d8a86455 --- /dev/null +++ b/test/v2_0/templates/groups/identified_mbox_sha1sum.json @@ -0,0 +1,12 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975", + "member": [ + { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_mbox_sha1sum_no_member.json b/test/v2_0/templates/groups/identified_mbox_sha1sum_no_member.json new file mode 100644 index 00000000..9469f56f --- /dev/null +++ b/test/v2_0/templates/groups/identified_mbox_sha1sum_no_member.json @@ -0,0 +1,5 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_openid.json b/test/v2_0/templates/groups/identified_openid.json new file mode 100644 index 00000000..6ff41177 --- /dev/null +++ b/test/v2_0/templates/groups/identified_openid.json @@ -0,0 +1,12 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "openid": "http://openid.example.org/12345", + "member": [ + { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } + ] +} \ No newline at end of file diff --git a/test/v2_0/templates/groups/identified_openid_no_member.json b/test/v2_0/templates/groups/identified_openid_no_member.json new file mode 100644 index 00000000..fa3a194c --- /dev/null +++ b/test/v2_0/templates/groups/identified_openid_no_member.json @@ -0,0 +1,5 @@ +{ + "objectType": "Group", + "name": "Group Identified", + "openid": "http://openid.example.org/12345" +} \ No newline at end of file diff --git a/test/v2_0/templates/results/default.json b/test/v2_0/templates/results/default.json new file mode 100644 index 00000000..81dd7af9 --- /dev/null +++ b/test/v2_0/templates/results/default.json @@ -0,0 +1,19 @@ +{ + "score": { + "scaled": 0.95, + "raw": 95, + "min": 0, + "max": 100 + }, + "extensions": { + "http://example.com/profiles/meetings/resultextensions/minuteslocation": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/resultextensions/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + }, + "success": true, + "completion": true, + "response": "We agreed on some example actions.", + "duration": "PT1H0M0S" +} \ No newline at end of file diff --git a/test/v2_0/templates/results/no_extensions.json b/test/v2_0/templates/results/no_extensions.json new file mode 100644 index 00000000..9de45f93 --- /dev/null +++ b/test/v2_0/templates/results/no_extensions.json @@ -0,0 +1,12 @@ +{ + "score": { + "scaled": 0.95, + "raw": 95, + "min": 0, + "max": 100 + }, + "success": true, + "completion": true, + "response": "We agreed on some example actions.", + "duration": "PT1H0M0S" +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/actor.json b/test/v2_0/templates/statements/actor.json new file mode 100644 index 00000000..72069e9c --- /dev/null +++ b/test/v2_0/templates/statements/actor.json @@ -0,0 +1,16 @@ +{ + "actor": { + "objectType": "Agent" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/attachment.json b/test/v2_0/templates/statements/attachment.json new file mode 100644 index 00000000..d463a50a --- /dev/null +++ b/test/v2_0/templates/statements/attachment.json @@ -0,0 +1,19 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + }, + "attachments": [] +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/authority.json b/test/v2_0/templates/statements/authority.json new file mode 100644 index 00000000..b2736478 --- /dev/null +++ b/test/v2_0/templates/statements/authority.json @@ -0,0 +1,21 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "authority": { + "objectType": "Agent" + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/context.json b/test/v2_0/templates/statements/context.json new file mode 100644 index 00000000..bfc04222 --- /dev/null +++ b/test/v2_0/templates/statements/context.json @@ -0,0 +1,19 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "context": { }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} diff --git a/test/v2_0/templates/statements/context_sans_objectType.json b/test/v2_0/templates/statements/context_sans_objectType.json new file mode 100644 index 00000000..682babf9 --- /dev/null +++ b/test/v2_0/templates/statements/context_sans_objectType.json @@ -0,0 +1,18 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "context": { }, + "object": { + "id": "http://www.example.com/meetings/occurances/34534" + } +} diff --git a/test/v2_0/templates/statements/default.json b/test/v2_0/templates/statements/default.json new file mode 100644 index 00000000..0828e0b2 --- /dev/null +++ b/test/v2_0/templates/statements/default.json @@ -0,0 +1,18 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/multiple_actor.json b/test/v2_0/templates/statements/multiple_actor.json new file mode 100644 index 00000000..dc835987 --- /dev/null +++ b/test/v2_0/templates/statements/multiple_actor.json @@ -0,0 +1,23 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + }, + "actor": { + "objectType": "Agent", + "name": "xAPI mbox2", + "mbox": "mailto:xapi2@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} diff --git a/test/v2_0/templates/statements/no_actor.json b/test/v2_0/templates/statements/no_actor.json new file mode 100644 index 00000000..e14511be --- /dev/null +++ b/test/v2_0/templates/statements/no_actor.json @@ -0,0 +1,13 @@ +{ + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/no_object.json b/test/v2_0/templates/statements/no_object.json new file mode 100644 index 00000000..6ca3ab01 --- /dev/null +++ b/test/v2_0/templates/statements/no_object.json @@ -0,0 +1,14 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/no_verb.json b/test/v2_0/templates/statements/no_verb.json new file mode 100644 index 00000000..dd7eeaa7 --- /dev/null +++ b/test/v2_0/templates/statements/no_verb.json @@ -0,0 +1,11 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/object_activity.json b/test/v2_0/templates/statements/object_activity.json new file mode 100644 index 00000000..36e13447 --- /dev/null +++ b/test/v2_0/templates/statements/object_activity.json @@ -0,0 +1,17 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "Activity" + } +} diff --git a/test/v2_0/templates/statements/object_actor.json b/test/v2_0/templates/statements/object_actor.json new file mode 100644 index 00000000..6dd00bb9 --- /dev/null +++ b/test/v2_0/templates/statements/object_actor.json @@ -0,0 +1,17 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "Agent" + } +} diff --git a/test/v2_0/templates/statements/object_agent_default.json b/test/v2_0/templates/statements/object_agent_default.json new file mode 100644 index 00000000..95884ee8 --- /dev/null +++ b/test/v2_0/templates/statements/object_agent_default.json @@ -0,0 +1,19 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/object_group_default.json b/test/v2_0/templates/statements/object_group_default.json new file mode 100644 index 00000000..ece50aba --- /dev/null +++ b/test/v2_0/templates/statements/object_group_default.json @@ -0,0 +1,19 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "Group", + "name": "Group Anonymous", + "mbox": "mailto:xapi@adlnet.gov" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/object_statementref.json b/test/v2_0/templates/statements/object_statementref.json new file mode 100644 index 00000000..669cf2ff --- /dev/null +++ b/test/v2_0/templates/statements/object_statementref.json @@ -0,0 +1,18 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "StatementRef", + "id": "8f87ccde-bb56-4c2e-ab83-44982ef22df0" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/object_statementref_no_id.json b/test/v2_0/templates/statements/object_statementref_no_id.json new file mode 100644 index 00000000..7755faff --- /dev/null +++ b/test/v2_0/templates/statements/object_statementref_no_id.json @@ -0,0 +1,17 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "StatementRef" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/object_substatement.json b/test/v2_0/templates/statements/object_substatement.json new file mode 100644 index 00000000..e3850139 --- /dev/null +++ b/test/v2_0/templates/statements/object_substatement.json @@ -0,0 +1,17 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "SubStatement" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/object_substatement_default.json b/test/v2_0/templates/statements/object_substatement_default.json new file mode 100644 index 00000000..7cae330d --- /dev/null +++ b/test/v2_0/templates/statements/object_substatement_default.json @@ -0,0 +1,33 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "object": { + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/result.json b/test/v2_0/templates/statements/result.json new file mode 100644 index 00000000..ccebed87 --- /dev/null +++ b/test/v2_0/templates/statements/result.json @@ -0,0 +1,19 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } + }, + "result": { }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/unicode.json b/test/v2_0/templates/statements/unicode.json new file mode 100644 index 00000000..f9ddaa37 --- /dev/null +++ b/test/v2_0/templates/statements/unicode.json @@ -0,0 +1,60 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended", + "ja-JP": "出席した", + "ko-KR": "참석", + "is-IS": "sótti", + "ru-RU": "участие", + "pa-IN": "ਹਾਜ਼ਰ", + "sk-SK": "zúčastnil", + "ar-EG": "حضر", + "hy-AM": "ներկա է գտնվել", + "kn-IN": "ಹಾಜರಿದ್ದರು" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/unicode", + "definition": { + "name": { + "en": "Other", + "en-GB": "attended", + "en-US": "attended", + "ja-JP": "出席した", + "ko-KR": "참석", + "is-IS": "sótti", + "ru-RU": "участие", + "pa-IN": "ਹਾਜ਼ਰ", + "sk-SK": "zúčastnil", + "ar-EG": "حضر", + "hy-AM": "ներկա է գտնվել", + "kn-IN": "ಹಾಜರಿದ್ದರು" + }, + "description": { + "en-US": "On this map, please mark Franklin, TN", + "en-GB": "On this map, please mark Franklin, TN" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "other", + "correctResponsesPattern": [ + "(35.937432,-86.868896)" + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } +} diff --git a/test/v2_0/templates/statements/verb.json b/test/v2_0/templates/statements/verb.json new file mode 100644 index 00000000..2544ad3b --- /dev/null +++ b/test/v2_0/templates/statements/verb.json @@ -0,0 +1,12 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/statements/voiding.json b/test/v2_0/templates/statements/voiding.json new file mode 100644 index 00000000..8e357899 --- /dev/null +++ b/test/v2_0/templates/statements/voiding.json @@ -0,0 +1,17 @@ +{ + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id":"http://adlnet.gov/expapi/verbs/voided", + "display":{ + "en-US":"voided" + } + }, + "object": { + "objectType": "StatementRef", + "id": "8f87ccde-bb56-4c2e-ab83-44982ef22df0" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/activity.json b/test/v2_0/templates/substatements/activity.json new file mode 100644 index 00000000..f3295dc5 --- /dev/null +++ b/test/v2_0/templates/substatements/activity.json @@ -0,0 +1,18 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity" + } +} diff --git a/test/v2_0/templates/substatements/activity_definition_description.json b/test/v2_0/templates/substatements/activity_definition_description.json new file mode 100644 index 00000000..479bf978 --- /dev/null +++ b/test/v2_0/templates/substatements/activity_definition_description.json @@ -0,0 +1,39 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present.", + "ase": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } +} diff --git a/test/v2_0/templates/substatements/activity_definition_description_invalid.json b/test/v2_0/templates/substatements/activity_definition_description_invalid.json new file mode 100644 index 00000000..a753a212 --- /dev/null +++ b/test/v2_0/templates/substatements/activity_definition_description_invalid.json @@ -0,0 +1,39 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present.", + "en-something-AU": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } +} diff --git a/test/v2_0/templates/substatements/activity_definition_name.json b/test/v2_0/templates/substatements/activity_definition_name.json new file mode 100644 index 00000000..b6e675ff --- /dev/null +++ b/test/v2_0/templates/substatements/activity_definition_name.json @@ -0,0 +1,39 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting", + "zh-Hans-CN": "例如会议" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } +} diff --git a/test/v2_0/templates/substatements/activity_definition_name_invalid.json b/test/v2_0/templates/substatements/activity_definition_name_invalid.json new file mode 100644 index 00000000..41136dae --- /dev/null +++ b/test/v2_0/templates/substatements/activity_definition_name_invalid.json @@ -0,0 +1,39 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534", + "definition": { + "type": "http://adlnet.gov/expapi/activities/meeting", + "name": { + "en-GB": "example meeting", + "en-US": "example meeting", + "zh-something-ccc": "例如会议" + }, + "description": { + "en-GB": "An example meeting that happened on a specific occasion with certain people present.", + "en-US": "An example meeting that happened on a specific occasion with certain people present." + }, + "moreInfo": "http://virtualmeeting.example.com/345256", + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } +} diff --git a/test/v2_0/templates/substatements/actor.json b/test/v2_0/templates/substatements/actor.json new file mode 100644 index 00000000..5ffb145f --- /dev/null +++ b/test/v2_0/templates/substatements/actor.json @@ -0,0 +1,17 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/agent.json b/test/v2_0/templates/substatements/agent.json new file mode 100644 index 00000000..bd47f118 --- /dev/null +++ b/test/v2_0/templates/substatements/agent.json @@ -0,0 +1,18 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Agent" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/agent_default.json b/test/v2_0/templates/substatements/agent_default.json new file mode 100644 index 00000000..7b02d354 --- /dev/null +++ b/test/v2_0/templates/substatements/agent_default.json @@ -0,0 +1,20 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Agent", + "name": "xAPI mbox", + "mbox": "mailto:xapi@adlnet.gov" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/context.json b/test/v2_0/templates/substatements/context.json new file mode 100644 index 00000000..b106b665 --- /dev/null +++ b/test/v2_0/templates/substatements/context.json @@ -0,0 +1,20 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "context": { }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/context_invalid_language_map.json b/test/v2_0/templates/substatements/context_invalid_language_map.json new file mode 100644 index 00000000..7f9130e2 --- /dev/null +++ b/test/v2_0/templates/substatements/context_invalid_language_map.json @@ -0,0 +1,36 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "context": { + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "revision": "rev_10_3_2", + "platform": "Example virtual meeting software", + "language": "something", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "extensions": { + "http://example.com/profiles/meetings/contextextensions/airspeed": "600mph", + "http://example.com/profiles/meetings/contextextensions/pilot": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} diff --git a/test/v2_0/templates/substatements/context_valid_language_map.json b/test/v2_0/templates/substatements/context_valid_language_map.json new file mode 100644 index 00000000..fd32b633 --- /dev/null +++ b/test/v2_0/templates/substatements/context_valid_language_map.json @@ -0,0 +1,36 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "context": { + "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", + "revision": "rev_10_3_2", + "platform": "Example virtual meeting software", + "language": "fr-CA", + "statement": { + "objectType": "StatementRef", + "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee" + }, + "extensions": { + "http://example.com/profiles/meetings/contextextensions/airspeed": "600mph", + "http://example.com/profiles/meetings/contextextensions/pilot": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} diff --git a/test/v2_0/templates/substatements/default.json b/test/v2_0/templates/substatements/default.json new file mode 100644 index 00000000..b1e49dac --- /dev/null +++ b/test/v2_0/templates/substatements/default.json @@ -0,0 +1,19 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/group.json b/test/v2_0/templates/substatements/group.json new file mode 100644 index 00000000..85a73b83 --- /dev/null +++ b/test/v2_0/templates/substatements/group.json @@ -0,0 +1,18 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Group" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/group_default.json b/test/v2_0/templates/substatements/group_default.json new file mode 100644 index 00000000..7e7ef889 --- /dev/null +++ b/test/v2_0/templates/substatements/group_default.json @@ -0,0 +1,20 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Group", + "name": "Group Anonymous", + "mbox": "mailto:xapi@adlnet.gov" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/interaction_component_invalid_language_map.json b/test/v2_0/templates/substatements/interaction_component_invalid_language_map.json new file mode 100644 index 00000000..740877e2 --- /dev/null +++ b/test/v2_0/templates/substatements/interaction_component_invalid_language_map.json @@ -0,0 +1,61 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Performance" + }, + "description": { + "en": "This interaction measures performance over a day of RS sports:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "performance", + "correctResponsesPattern": [ + "pong[.]1:[,]dg[.]:10[,]lunch[.]" + ], + "steps": [ + { + "id": "pong", + "description": { + "en-US": "Net pong matches won" + } + }, + { + "id": "dg", + "description": { + "en-US": "Strokes over par in disc golf at Liberty" + } + }, + { + "id": "lunch", + "description": { + "en-US": "Lunch having been eaten", + "something": "昼食は食べれました" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + } +} diff --git a/test/v2_0/templates/substatements/interaction_component_valid_language_map.json b/test/v2_0/templates/substatements/interaction_component_valid_language_map.json new file mode 100644 index 00000000..f2266b5f --- /dev/null +++ b/test/v2_0/templates/substatements/interaction_component_valid_language_map.json @@ -0,0 +1,94 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/categories/teammeeting", + "definition": { + "name": { + "en": "Matching" + }, + "description": { + "en": "Match these people to their kickball team:" + }, + "type": "http://adlnet.gov/expapi/activities/cmi.interaction", + "moreInfo": "http://virtualmeeting.example.com/345256", + "interactionType": "matching", + "correctResponsesPattern": [ + "ben[.]3[,]chris[.]2[,]troy[.]4[,]freddie[.]1" + ], + "source": [ + { + "id": "ben", + "description": { + "en-US": "Ben" + } + }, + { + "id": "chris", + "description": { + "en-US": "Chris" + } + }, + { + "id": "troy", + "description": { + "en-US": "Troy" + } + }, + { + "id": "freddie", + "description": { + "en-US": "Freddie" + } + } + ], + "target": [ + { + "id": "1", + "description": { + "en-US": "Swift Kick in the Grass", + "ja": "草でスイフトキック" + } + }, + { + "id": "2", + "description": { + "en-US": "We got Runs" + } + }, + { + "id": "3", + "description": { + "en-US": "Duck" + } + }, + { + "id": "4", + "description": { + "en-US": "Van Delay Industries" + } + } + ], + "extensions": { + "http://example.com/profiles/meetings/extension/location": "X:\\meetings\\minutes\\examplemeeting.one", + "http://example.com/profiles/meetings/extension/reporter": { + "name": "Thomas", + "id": "http://openid.com/342" + } + } + } + + } +} diff --git a/test/v2_0/templates/substatements/no_actor.json b/test/v2_0/templates/substatements/no_actor.json new file mode 100644 index 00000000..c4ce1a6b --- /dev/null +++ b/test/v2_0/templates/substatements/no_actor.json @@ -0,0 +1,14 @@ +{ + "objectType": "SubStatement", + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/no_object.json b/test/v2_0/templates/substatements/no_object.json new file mode 100644 index 00000000..5bf02181 --- /dev/null +++ b/test/v2_0/templates/substatements/no_object.json @@ -0,0 +1,15 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/no_verb.json b/test/v2_0/templates/substatements/no_verb.json new file mode 100644 index 00000000..1c883ae6 --- /dev/null +++ b/test/v2_0/templates/substatements/no_verb.json @@ -0,0 +1,12 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/result.json b/test/v2_0/templates/substatements/result.json new file mode 100644 index 00000000..58ef2cbf --- /dev/null +++ b/test/v2_0/templates/substatements/result.json @@ -0,0 +1,20 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "result": { }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/statementref.json b/test/v2_0/templates/substatements/statementref.json new file mode 100644 index 00000000..a205e545 --- /dev/null +++ b/test/v2_0/templates/substatements/statementref.json @@ -0,0 +1,19 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "StatementRef", + "id": "8f87ccde-bb56-4c2e-ab83-44982ef22df0" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/statementref_no_id.json b/test/v2_0/templates/substatements/statementref_no_id.json new file mode 100644 index 00000000..05f571db --- /dev/null +++ b/test/v2_0/templates/substatements/statementref_no_id.json @@ -0,0 +1,18 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported" + } + }, + "object": { + "objectType": "StatementRef" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/verb.json b/test/v2_0/templates/substatements/verb.json new file mode 100644 index 00000000..535512f1 --- /dev/null +++ b/test/v2_0/templates/substatements/verb.json @@ -0,0 +1,13 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI account", + "mbox": "mailto:xapi@adlnet.gov" + }, + "verb": { }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/substatements/verb_invalid.json b/test/v2_0/templates/substatements/verb_invalid.json new file mode 100644 index 00000000..764fc04c --- /dev/null +++ b/test/v2_0/templates/substatements/verb_invalid.json @@ -0,0 +1,20 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported", + "s-something": "пријавио" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} diff --git a/test/v2_0/templates/substatements/verb_valid.json b/test/v2_0/templates/substatements/verb_valid.json new file mode 100644 index 00000000..650d1f8d --- /dev/null +++ b/test/v2_0/templates/substatements/verb_valid.json @@ -0,0 +1,20 @@ +{ + "objectType": "SubStatement", + "actor": { + "objectType": "Agent", + "name": "xAPI mbox_sha1sum", + "mbox_sha1sum": "cd9b00a5611f94eaa7b1661edab976068e364975" + }, + "verb": { + "id": "http://adlnet.gov/expapi/verbs/reported", + "display": { + "en-GB": "reported", + "en-US": "reported", + "sr-Cyrl": "пријавио" + } + }, + "object": { + "objectType": "Activity", + "id": "http://www.example.com/meetings/occurances/34534" + } +} diff --git a/test/v2_0/templates/verbs/default.json b/test/v2_0/templates/verbs/default.json new file mode 100644 index 00000000..dd12f9e9 --- /dev/null +++ b/test/v2_0/templates/verbs/default.json @@ -0,0 +1,7 @@ +{ + "id": "http://adlnet.gov/expapi/verbs/attended", + "display": { + "en-GB": "attended", + "en-US": "attended" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/verbs/no_display.json b/test/v2_0/templates/verbs/no_display.json new file mode 100644 index 00000000..d4131083 --- /dev/null +++ b/test/v2_0/templates/verbs/no_display.json @@ -0,0 +1,3 @@ +{ + "id": "http://adlnet.gov/expapi/verbs/attended" +} \ No newline at end of file diff --git a/test/v2_0/templates/verbs/no_id.json b/test/v2_0/templates/verbs/no_id.json new file mode 100644 index 00000000..63a51496 --- /dev/null +++ b/test/v2_0/templates/verbs/no_id.json @@ -0,0 +1,6 @@ +{ + "display": { + "en-GB": "attended", + "en-US": "attended" + } +} \ No newline at end of file diff --git a/test/v2_0/templates/verbs/voided.json b/test/v2_0/templates/verbs/voided.json new file mode 100644 index 00000000..08785c4f --- /dev/null +++ b/test/v2_0/templates/verbs/voided.json @@ -0,0 +1,6 @@ +{ + "id":"http://adlnet.gov/expapi/verbs/voided", + "display":{ + "en-US":"voided" + } +} \ No newline at end of file diff --git a/test/v2_0/util/requests.js b/test/v2_0/util/requests.js new file mode 100644 index 00000000..a569c75c --- /dev/null +++ b/test/v2_0/util/requests.js @@ -0,0 +1,418 @@ +const path = require("path"); +const axiosBase = require("axios"); +const axios = axiosBase.default; +const addOAuthInterceptor = require("axios-oauth-1.0a").default; +const chai = require("chai"); +const oldHelpers = require("../../helper"); + +/** Test directory */ +const DIRECTORY = process.env.DIRECTORY; + +/** Defines endpoint of the LRS you are testing. Currently assumes authentication is not required */ +const LRS_ENDPOINT = process.env.LRS_ENDPOINT; + +/** Appears to use absolute path */ +const TEMPLATE_FOLDER = './test/' + process.env.DIRECTORY + '/templates'; + +/** Appears to use relative path */ +const TEMPLATE_FOLDER_RELATIVE = './' + process.env.DIRECTORY + '/templates'; + +/** Endpoint About */ +const PATH_ABOUT = '/about'; + +/** Endpoint Activities */ +const PATH_ACTIVITIES = '/activities'; + +/** Endpoint Activities Profile */ +const PATH_ACTIVITIES_PROFILE = '/activities/profile'; + +/** Endpoint Activities State */ +const PATH_ACTIVITIES_STATE = '/activities/state'; + +/** Endpoint Agents */ +const PATH_AGENTS = '/agents'; + +/** Endpoint Agents Profile */ +const PATH_AGENTS_PROFILE = '/agents/profile'; + +/** Endpoint Statements */ +const PATH_STATEMENTS = '/statements'; + +/** Assign the default headers for our setup */ +axios.defaults.headers.common = { + ...axios.defaults.headers.common, + + "Content-Type": "application/json", + "X-Experience-API-Version": process.env.XAPI_VERSION, +} + +/** + * Configure our auth setup. + * + * If they're using OAuth, then a global value will know about it, + * but the basic auth values are written into the process env parser. + */ +if (global.OAUTH != undefined) { + addOAuthInterceptor(axios, { + algorithm: "HMAC-SHA1", + key: global.OAUTH.consumer_key, + secret: global.OAUTH.consumer_secret, + token: global.OAUTH.token, + tokenSecret: global.OAUTH.token_secret, + verifier: global.OAUTH.verifier, + }); +} +else { + let user = process.env.BASIC_AUTH_USER; + let pass = process.env.BASIC_AUTH_PASSWORD; + + axios.defaults.headers.common["Authorization"] = "Basic " + Buffer.from(user + ':' + pass).toString("base64"); +} + +/** + * + * @param {String} endpoint + * @param {String} path + */ +function joinPaths(endpoint, path) { + return path + ? endpoint.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '') + : endpoint; +} + +const requests = { + + resourcePaths: { + activityState: PATH_ACTIVITIES_STATE, + activityProfile: PATH_ACTIVITIES_PROFILE, + agentsProfile: PATH_AGENTS_PROFILE, + }, + + /** + * POST an xAPI statement to the LRS. + * @param {Object} statement The xAPI statement to send. + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + getStatementExact: async(id, headerOverrides) => { + let params = { + statementId: id + }; + + return requests.getDocuments(PATH_STATEMENTS, params, { + headers: headerOverrides + }) + .catch(err => err.response); + }, + + /** + * POST an xAPI statement to the LRS. + * @param {Object} statement The xAPI statement to send. + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + getStatementExactPromise: async(id, headerOverrides) => { + let endpoint = joinPaths(LRS_ENDPOINT, PATH_STATEMENTS); + let params = { + statementId: id + }; + + return requests.getDocuments(endpoint, params, { + headers: headerOverrides + }) + .catch(error => error.response); + }, + + /** + * + * @returns {String} Boundary string. + */ + generateRandomMultipartBoundary: () => { + return `-------------__${oldHelpers.generateUUID()}__123__456`; + }, + + /** + * Create a multipart/mixed body from a given statement + a boundary. + * @param {Object} statement + * @param {string} boundary + * @returns + */ + generateSignedStatementBody: (statement, boundary) => { + + let multipartBoundary = (boundary || requests.generateRandomMultipartBoundary()); + let multipartBody = oldHelpers.signStatement(statement, {boundary: multipartBoundary}); + + return multipartBody.toString(); + }, + + /** + * POST an xAPI statement to the LRS. + * @param {Object} multipartBody The signed multipart body. + * @param {string} boundary The multipart boundary used to create this body. + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + sendSignedStatementBody: async(multipartBody, boundary, headerOverrides) => { + + let endpoint = joinPaths(LRS_ENDPOINT, PATH_STATEMENTS); + + return axios.post(endpoint, multipartBody, { + headers: { + ...headerOverrides, + "Content-Type": `multipart/mixed; boundary=${boundary}` + } + }) + .catch(err => err.response); + }, + + /** + * POST an xAPI statement to the LRS. + * @param {Object} statement The xAPI statement to send. + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + sendStatement: async(statement, headerOverrides) => { + let endpoint = joinPaths(LRS_ENDPOINT, PATH_STATEMENTS); + + return axios.post(endpoint, statement, { + headers: headerOverrides + }) + .catch(err => err.response) + }, + + /** + * POST an xAPI statement to the LRS. + * @param {Object} statement The xAPI statement to send. + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + sendStatementPromise: async(statement, headerOverrides) => { + let endpoint = joinPaths(LRS_ENDPOINT, PATH_STATEMENTS); + + return axios.post(endpoint, statement, { + headers: headerOverrides + }) + .catch(err => err.response); + }, + + /** + * GET an Activity from the LRS's Activity Resource endpoint. + * @param {string} iri IRI for the Activity. + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + getActivityWithIRI: async(iri, headerOverrides) => { + let endpoint = joinPaths(LRS_ENDPOINT, PATH_ACTIVITIES); + let query = `?activityId=${encodeURIComponent(iri)}`; + + return await axios.get(endpoint + query, { + headers: headerOverrides + }) + .catch(err => err.response); + }, + + /** + * @typedef {Object} ActivityStateParameters + * @property {string} activityId The Activity id associated with this state. + * @property {Object} agent The Agent or Identified Group associated with this state. + * @property {string} registration The registration associated with this state. + * @property {string} stateId The id for this state, within the given context. + */ + /** + * @typedef {ActivityStateParameters} ActivityStateGetParameters + * @property {string} since Additional Timestamp parameter for multiple document requests. DO NOT USE FOR SINGLE DOCUMENTS. + */ + /** + * PUT a document into the Activity State resource. + * @param {any} state Generic state document to store. + * @param {ActivityStateParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + putState: async(state, params, headerOverrides) => { + return requests.putDocument(PATH_ACTIVITIES_STATE, state, params, headerOverrides); + }, + + /** + * POST a document into the Activity State resource. + * @param {any} state Generic state document to store. + * @param {ActivityStateParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + postState: async(state, params, headerOverrides) => { + return requests.postDocument(PATH_ACTIVITIES_STATE, state, params, headerOverrides); + }, + + /** + * DELETE a document into the Activity State resource. + * @param {ActivityStateParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + deleteState: async(params, headerOverrides) => { + return requests.deleteDocument(PATH_ACTIVITIES_STATE, params, headerOverrides); + }, + + /** + * GET one or multiple documents from the Activity State resource. + * @param {ActivityStateParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + getSingleState: async(params, headerOverrides) => { + return requests.getDocuments(PATH_ACTIVITIES_STATE, params, headerOverrides); + }, + + /** + * GET one or multiple documents from the Activity State resource. + * @param {ActivityStateGetParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + getMultipleStates: async(params, headerOverrides) => { + return requests.getDocuments(PATH_ACTIVITIES_STATE, params, headerOverrides); + }, + + /** + * @typedef {Object} AgentProfileParameters + * @property {string} profileId The Activity id associated with this state. + * @property {Object} agent The Agent or Identified Group associated with this state. + */ + /** + * @typedef {Object} AgentProfileMultipleGetParameters + * @property {Object} agent The Agent or Identified Group associated with this state. + * @property {string} since Additional Timestamp parameter for multiple document requests. DO NOT USE FOR SINGLE DOCUMENTS. + */ + /** + * PUT a document into the Activity State resource. + * @param {any} document Generic state document to store. + * @param {AgentProfileParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + putAgentProfile: async(document, params, headerOverrides) => { + return requests.putDocument(PATH_AGENTS_PROFILE, document, params, headerOverrides); + }, + + /** + * POST a document into the Activity State resource. + * @param {any} document Generic state document to store. + * @param {AgentProfileParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + postAgentProfile: async(document, params, headerOverrides) => { + return requests.postDocument(PATH_AGENTS_PROFILE, document, params, headerOverrides); + }, + + /** + * DELETE a document into the Activity State resource. + * @param {ActivityStateParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + deleteAgentProfile: async(params, headerOverrides) => { + return requests.deleteDocument(PATH_AGENTS_PROFILE, params, headerOverrides); + }, + + /** + * GET one or multiple documents from the Activity State resource. + * @param {ActivityStateParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + getSingleAgentProfile: async(params, headerOverrides) => { + return requests.getDocuments(PATH_AGENTS_PROFILE, params, headerOverrides); + }, + + /** + * GET one or multiple documents from the Activity State resource. + * @param {AgentProfileMultipleGetParameters} params + * @param {Object} headerOverrides Headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + getMultipleAgentProfiles: async(params, headerOverrides) => { + return requests.getDocuments(PATH_AGENTS_PROFILE, params, headerOverrides); + }, + + /** + * Get a generic document from a generic resource path. + * @param {string} resourcePath Relative path to the resource endpoint, relative to the LRS's base xAPI path. + * @param {Object} params Query parameters for the request. + * @param {Object} headerOverrides Optional headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + getDocuments: async(resourcePath, params, headerOverrides) => { + let endpoint = joinPaths(LRS_ENDPOINT, resourcePath); + let query = "?" + oldHelpers.getUrlEncoding(params); + + return axios.get(endpoint + query, { + headers: headerOverrides + }) + .catch(err => err.response); + }, + + /** + * PUT a generic document to a generic resource path. + * @param {string} resourcePath Relative path to the resource endpoint, relative to the LRS's base xAPI path. + * @param {any} document Document to send. + * @param {Object} params Query parameters for the request. + * @param {Object} headerOverrides Optional headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + putDocument: async(resourcePath, document, params, headerOverrides) => { + let endpoint = joinPaths(LRS_ENDPOINT, resourcePath); + let query = "?" + oldHelpers.getUrlEncoding(params); + + return axios.put(endpoint + query, document, { + headers: headerOverrides + }) + .catch(err => err.response); + }, + + /** + * POST a generic document to a generic resource path. + * @param {string} resourcePath Relative path to the resource endpoint, relative to the LRS's base xAPI path. + * @param {any} document Document to send. + * @param {Object} params Query parameters for the request. + * @param {Object} headerOverrides Optional headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + postDocument: async(resourcePath, document, params, headerOverrides) => { + let endpoint = joinPaths(LRS_ENDPOINT, resourcePath); + let query = "?" + oldHelpers.getUrlEncoding(params); + + return axios.post(endpoint + query, document, { + headers: headerOverrides + }) + .catch(err => err.response); + }, + + /** + * DELETE a generic document to a generic resource path. + * @param {string} resourcePath Relative path to the resource endpoint, relative to the LRS's base xAPI path. + * @param {Object} params Query parameters for the request. + * @param {Object} headerOverrides Optional headers to override for this request. + * @returns {Promise} The LRS's simplified response. + */ + deleteDocument: async(resourcePath, params, headerOverrides) => { + let endpoint = joinPaths(LRS_ENDPOINT, resourcePath); + let query = "?" + oldHelpers.getUrlEncoding(params); + + return axios.delete(endpoint + query, { + headers: headerOverrides + }) + .catch(err => err.response); + }, + + /** + * Await a delay for a given number of milliseconds. + * @param {Number} ms Milliseconds to delay + */ + delay: async(ms) => { + return new Promise((res, _) => setTimeout(res, ms)); + } +}; + +module.exports = requests; diff --git a/version.js b/version.js index b43cc468..a7234dd3 100644 --- a/version.js +++ b/version.js @@ -1,3 +1,4 @@ module.exports = { - "versionNumber": "1.0.3.18" + "versionNumber": "2.0.0.0", + "versionNumberLegacy": "1.0.3.18" }