├── .githooks └── pre-commit ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── run_tests.yml ├── .gitignore ├── CHANGELOG.md ├── CLAUDE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── SECURITY.md ├── UPGRADES.md ├── composer.json ├── phpstan.neon ├── phpunit.xml.dist ├── pint.json ├── src ├── Concerns │ ├── Arrayable.php │ └── JsonStringable.php ├── DeepLinkResources │ ├── DateTimeInterval.php │ ├── HasDimensions.php │ ├── Icon.php │ ├── Iframe.php │ ├── Resource.php │ └── Window.php ├── Helpers │ └── Helpers.php ├── Interfaces │ ├── ICache.php │ ├── ICookie.php │ ├── IDatabase.php │ ├── ILtiDeployment.php │ ├── ILtiRegistration.php │ ├── ILtiServiceConnector.php │ ├── IMessageValidator.php │ ├── IMigrationDatabase.php │ └── IServiceRequest.php ├── JwksEndpoint.php ├── Lti1p1Key.php ├── LtiAbstractService.php ├── LtiAssignmentsGradesService.php ├── LtiConstants.php ├── LtiCourseGroupsService.php ├── LtiDeepLink.php ├── LtiDeployment.php ├── LtiException.php ├── LtiGrade.php ├── LtiGradeSubmissionReview.php ├── LtiLineitem.php ├── LtiMessageLaunch.php ├── LtiNamesRolesProvisioningService.php ├── LtiOidcLogin.php ├── LtiRegistration.php ├── LtiServiceConnector.php ├── MessageValidators │ ├── AbstractMessageValidator.php │ ├── DeepLinkMessageValidator.php │ ├── ResourceMessageValidator.php │ └── SubmissionReviewMessageValidator.php ├── OidcException.php └── ServiceRequest.php └── tests ├── Certification └── Lti13CertificationTest.php ├── DeepLinkResources ├── DateTimeIntervalTest.php ├── IconTest.php ├── IframeTest.php ├── ResourceTest.php └── WindowTest.php ├── Helpers └── HelpersTest.php ├── JwksEndpointTest.php ├── Lti1p1KeyTest.php ├── LtiAssignmentsGradesServiceTest.php ├── LtiCourseGroupsServiceTest.php ├── LtiDeepLinkTest.php ├── LtiDeploymentTest.php ├── LtiGradeSubmissionReviewTest.php ├── LtiGradeTest.php ├── LtiLineitemTest.php ├── LtiMessageLaunchTest.php ├── LtiNamesRolesProvisioningServiceTest.php ├── LtiOidcLoginTest.php ├── LtiRegistrationTest.php ├── LtiServiceConnectorTest.php ├── MessageValidators ├── DeepLinkMessageValidatorTest.php ├── ResourceMessageValidatorTest.php └── SubmissionReviewMessageValidatorTest.php ├── ServiceRequestTest.php ├── TestCase.php └── data ├── certification ├── invalid │ ├── Correct KID Required in Header │ │ ├── header.json │ │ └── payload.json │ ├── Exp and Iat Fields invalid │ │ ├── keep.json │ │ └── payload.json │ ├── Incorrect KID passed in JWT Header │ │ ├── header.json │ │ └── payload.json │ ├── JWT Passed is Not LTI 1.3 JWT │ │ ├── header.json │ │ └── payload.json │ ├── LTI version passed is not 1.3 │ │ └── payload.json │ ├── Launch Instructor With Email No Context │ │ └── payload.json │ ├── Launch With Missing resource_link_id │ │ └── payload.json │ ├── No LTI Version Passed in JWT │ │ └── payload.json │ ├── One or more JWT fields missing │ │ └── payload.json │ ├── deployment_id Claim Missing │ │ └── payload.json │ ├── message_type Claim Missing │ │ └── payload.json │ ├── role Claim Missing │ │ └── payload.json │ └── user Claim Missing │ │ └── payload.json └── valid │ ├── Assignments and Grades - Core Launch │ └── payload.json │ ├── Deep Linking - Core Launch of Passed LTI Link │ └── payload.json │ ├── Deep Linking - Send Deep Linking Request Payload │ └── payload.json │ ├── Launch Instructor No PII │ └── payload.json │ ├── Launch Instructor Only Email │ └── payload.json │ ├── Launch Instructor Only Names │ └── payload.json │ ├── Launch Instructor With No Role │ └── payload.json │ ├── Launch Instructor with Multiple Role Values │ └── payload.json │ ├── Launch Instructor with Short Role Value │ └── payload.json │ ├── Launch Instructor with Unknown Role │ └── payload.json │ ├── Launch LTI 1.3 Message as Instructor │ └── payload.json │ ├── Launch LTI 1.3 Message as Student │ └── payload.json │ ├── Launch Student No PII │ └── payload.json │ ├── Launch Student Only Email │ └── payload.json │ ├── Launch Student Only Names │ └── payload.json │ ├── Launch Student Only Resource Link │ └── payload.json │ ├── Launch Student With No Role │ └── payload.json │ ├── Launch Student with Multiple Role Values │ └── payload.json │ ├── Launch Student with Short Role Value │ └── payload.json │ ├── Launch Student with Unknown Role │ └── payload.json │ └── Names and Roles - Core Launch if Needed │ └── payload.json ├── private.key └── public.key /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | 5 | GIT_ROOT=$(git rev-parse --show-toplevel) # ".../lti-1-3-php-library" 6 | 7 | # So we avoid the "Not a git repository" error when performing git commands in a subdir 8 | unset GIT_DIR 9 | 10 | # Get changed files to be committed, excluding deleted files (since we can't grep them) 11 | CHANGED_FILES=$(git diff --cached --name-only --diff-filter=d) 12 | 13 | NO_FORMAT="\e[0m" 14 | F_BOLD="\e[1m" 15 | C_RED="\e[31m" 16 | C_YELLOW="\e[93m" 17 | C_CYAN="\e[36m" 18 | C_LIME="\e[92m" 19 | 20 | # Rather than doing `return 1`, we fail fast. 21 | function fail { 22 | echo -e "${C_RED}${F_BOLD}Pre-commit hook failed! Fix the above errors before committing.${NO_FORMAT}" 23 | exit 1 24 | } 25 | 26 | function file_ends_with_newline { 27 | file_path=$1 28 | 29 | # NOTE: Empty files technically end with a newline. 30 | [[ $(wc -l < "$file_path") -eq 0 ]] || [[ $(tail -c1 "$file_path" | wc -l) -gt 0 ]] 31 | } 32 | 33 | function is_executable_installed { 34 | executable_name=$1 35 | 36 | which "$executable_name" >/dev/null 37 | } 38 | 39 | # Returns a 0 status code if the given feature is enabled, 1 otherwise. 40 | # Feature names are arbitrarily defined in the optional file `.skipped-checks` 41 | # in order to give more control to developers to-as what gets executed. 42 | function feature_is_enabled { 43 | feature_name=$1 44 | 45 | # We redirect output so that it doesn't emit warnings if the file doesn't exist. 46 | ! grep "$feature_name" "$GIT_ROOT/.githooks/.skipped-checks" &> /dev/null 47 | } 48 | 49 | function feature_is_disabled { 50 | feature_name=$1 51 | 52 | ! feature_is_enabled "$feature_name" 53 | } 54 | 55 | function skip_if_no_changes { 56 | if [[ -z "$CHANGED_FILES" ]]; then 57 | echo "No changes were detected while running the pre-commit hook." && exit 0 58 | fi 59 | } 60 | 61 | function skip_if_merge_in_progress { 62 | if [ -f ".git/MERGE_HEAD" ]; then 63 | echo "Detected merge in progress, skipping pre-commit hook." && exit 0 64 | fi 65 | } 66 | 67 | function fail_if_unresolved_merge_conflict { 68 | # Check the files to prevent merge markers from being committed. 69 | if echo "$CHANGED_FILES" | xargs --no-run-if-empty egrep '[><]{7}' -H -I --line-number; then 70 | echo -e "${C_RED}You have merge markers (conflicts) in the above files, lines. Fix them before committing.${NO_FORMAT}" && fail 71 | fi 72 | } 73 | 74 | function lint_eof_newlines { 75 | if feature_is_disabled "pre-commit-auto-newlines"; then 76 | return 0 77 | fi 78 | 79 | text_files=$(echo "$CHANGED_FILES" | grep -E '\.(css|docker|Dockerfile|dockerignore|ejs|env|example|gitignore|html|js|json|php|py|rb|scss|sh|svg|toml|trivyignore|ts|txt|yaml|yml)$') 80 | for f in $text_files; do 81 | # Add a linebreak to the file if it doesn't have one 82 | if ! file_ends_with_newline "$f"; then 83 | echo >>"$f" 84 | git add "$f" 85 | fi 86 | done 87 | } 88 | 89 | function lint_php { 90 | php_files=$(echo "$CHANGED_FILES" | grep '\.php') 91 | if [[ -z "$php_files" ]]; then 92 | return 0 # There's nothing to lint. 93 | fi 94 | 95 | pint="vendor/bin/pint" 96 | 97 | if ! [ -x "$pint" ]; then 98 | echo -e "${C_RED}Pint is not installed. Install it with \`composer install\`.${NO_FORMAT}" && return 1 99 | fi 100 | 101 | php_files_arg=$(echo "$php_files" | tr '\n' ' ') 102 | 103 | echo -e "${C_CYAN}Linting Pint...${NO_FORMAT}" 104 | $pint || return 1 105 | 106 | git add $php_files_arg 107 | } 108 | 109 | echo -e "${NO_FORMAT}${F_BOLD}Running pre-commit hook...${NO_FORMAT}" 110 | 111 | skip_if_merge_in_progress 112 | skip_if_no_changes 113 | 114 | fail_if_unresolved_merge_conflict 115 | 116 | lint_eof_newlines || fail 117 | lint_php || fail 118 | 119 | echo -e "${C_LIME}${F_BOLD}Pre-commit hook passed!${NO_FORMAT}" 120 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dbhynds 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests & Style Checks 2 | on: 3 | # Trigger on any PR being opened 4 | pull_request: 5 | # Or weekly and on a merge to master (to update the badge) 6 | push: 7 | branches: 8 | - master 9 | schedule: 10 | - cron: 0 0 * * 0 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: "ramsey/composer-install@v2" 18 | - name: Check style 19 | run: composer lint 20 | 21 | test: 22 | name: Test 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | php: 27 | - "8.1" 28 | - "latest" 29 | steps: 30 | - uses: "actions/checkout@v3" 31 | - uses: "shivammathur/setup-php@v2" 32 | with: 33 | php-version: "${{ matrix.php }}" 34 | - uses: "ramsey/composer-install@v2" 35 | with: 36 | dependency-versions: "${{ matrix.dependencies }}" 37 | composer-options: "${{ matrix.composer-options }}" 38 | - name: Run tests 39 | run: composer test 40 | 41 | coverage: 42 | name: Code Coverage 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: "actions/checkout@v3" 46 | - uses: "shivammathur/setup-php@v2" 47 | with: 48 | php-version: latest 49 | coverage: xdebug 50 | - uses: "ramsey/composer-install@v2" 51 | - uses: paambaati/codeclimate-action@v5.0.0 52 | env: 53 | CC_TEST_REPORTER_ID: "${{ secrets.CC_TEST_REPORTER_ID }}" 54 | with: 55 | coverageCommand: composer test 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .idea 4 | .phpunit.result.cache 5 | .php_cs.cache 6 | .php-cs-fixer.cache 7 | .pint.cache 8 | .vscode 9 | 10 | build 11 | composer.lock 12 | composer.phar 13 | tests/_output/* 14 | tests/_support/_generated/* 15 | vendor 16 | 17 | # ignore the coverage folders 18 | **/.phpunit.cache 19 | **/coverage 20 | 21 | **/.claude/settings.local.json 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 5.3.2 2 | 3 | Fix URL for updating a line item [#89](https://github.com/packbackbooks/lti-1-3-php-library/pull/89). 4 | 5 | ## 5.3.1 6 | 7 | Validate scopes before a request [#86](https://github.com/packbackbooks/lti-1-3-php-library/pull/86). 8 | 9 | ## 5.3.0 10 | 11 | Add `AssignmentGradesService::deleteLineitem()` to update a line item [#83](https://github.com/packbackbooks/lti-1-3-php-library/pull/83). 12 | 13 | ## 5.1.7 14 | 15 | Add `AssignmentGradesService::updateLineitem()` to update a line item [#58](https://github.com/packbackbooks/lti-1-3-php-library/pull/58). 16 | 17 | ## 5.1.0 18 | 19 | Add `AssignmentGradesService::getLineItem()` to fetch a single line item [#47](https://github.com/packbackbooks/lti-1-3-php-library/pull/47). 20 | 21 | ## 5.0.0 22 | 23 | Implemented several changes to comply with (OpenID Connect Core)[https://openid.net/specs/openid-connect-core-1_0.html]. Nonce validation changed such that it now verifies that the nonce and state associated with an LTI Message Launch request matches the state associated with the nonce and state created during the OIDC login request. 24 | 25 | * Implement cryptographically secure methods for generating cookies and nonces for OIDC login requests. 26 | * Fix an issue with nonce validation not checking against the value in the Authentication Request. 27 | * Add stricter typing to `ICache` and `ICookie`. 28 | * Rename the `ICache::getNonce()` method to `ICache::checkNonceIsValid()`. 29 | * Improve how line items are fetched and created when putting a grade to a line item. ([#44](https://github.com/packbackbooks/lti-1-3-php-library/pull/44)) 30 | 31 | ## 4.1.3 32 | 33 | Updated the ImsCache to properly retrieve the nonce and request body. ([#40](https://github.com/packbackbooks/lti-1-3-php-library/pull/40)) 34 | 35 | ## 4.1.2 36 | 37 | Fixed a typing error when the response body of a request is null. ([#39](https://github.com/packbackbooks/lti-1-3-php-library/pull/39)) 38 | 39 | ## 4.1.1 40 | 41 | * Updated `LtiMessageLaunch` to fetch JWKs via an HTTP client instead of `file_get_contents()`. ([#38](https://github.com/packbackbooks/lti-1-3-php-library/pull/38)) 42 | * Added new methods to `ILtiServiceConnector`: `makeRequest()`, `getRequestBody()`. ([#38](https://github.com/packbackbooks/lti-1-3-php-library/pull/38)) 43 | 44 | ## 4.1.0 45 | 46 | * Allowed `getGrades()` to be called without a line item. ([#34](https://github.com/packbackbooks/lti-1-3-php-library/pull/34)) 47 | * Fixed fetching of line items and eliminated a PHP warning for missing key. ([#35](https://github.com/packbackbooks/lti-1-3-php-library/pull/35)) 48 | * Included the `resourceLinkId` attribute when creating a line item. ([#36](https://github.com/packbackbooks/lti-1-3-php-library/pull/36)) 49 | 50 | ## 4.0.0 51 | 52 | * Added a new method to `ILtiServiceConnector`: `setDebuggingMode()`. ([#32](https://github.com/packbackbooks/lti-1-3-php-library/pull/32)) 53 | 54 | ## 3.0.3 55 | 56 | * Added response/request logging to `LtiServiceConnector`. ([#31](https://github.com/packbackbooks/lti-1-3-php-library/pull/31)) 57 | 58 | Note: Upgrade to 4.0.0 for this logging to be disabled by default. 59 | 60 | ## 3.0.2 61 | 62 | * Fixed grades with a score of 0 not being synced. ([#30](https://github.com/packbackbooks/lti-1-3-php-library/pull/30)) 63 | 64 | ## 3.0.1 65 | 66 | * Fixed a few minor errors related to array indexes. ([#27](https://github.com/packbackbooks/lti-1-3-php-library/pull/27), [#28](https://github.com/packbackbooks/lti-1-3-php-library/pull/28), [#29](https://github.com/packbackbooks/lti-1-3-php-library/pull/29)) 67 | * Increased test coverage on the LtiMessageLaunch. ([#28](https://github.com/packbackbooks/lti-1-3-php-library/pull/28)) 68 | 69 | ## 3.0.0 70 | 71 | * Added a new method to `ICache`: `clearAccessToken()`. 72 | * Modified the constructor arguments for `LtiServiceConnector`, `LtiAssignmentGradesService`, `LtiCourseGroupsService`, and `LtiNamesRolesProvisioningService`. 73 | 74 | ## 2.0.3 75 | 76 | * Made an optimization to the logic added in 2.0.2. ([#19](https://github.com/packbackbooks/lti-1-3-php-library/pull/19)) 77 | 78 | ## 2.0.2 79 | 80 | * Fixed pagination of lineitems in the `LtiAssignmentGradesService` and makes it possible to get all lineitems with a single function call. ([#17](https://github.com/packbackbooks/lti-1-3-php-library/pull/17)) 81 | 82 | ## 2.0.1 83 | 84 | * Fixed a bug in the `LtiServiceConnector` that was causing double-encoded JSON to be sent in POST bodies, introduced in 2.0.0. ([#15](https://github.com/packbackbooks/lti-1-3-php-library/pull/15)) 85 | 86 | ## 2.0.0 87 | 88 | * A standard naming convention was implemented for all interfaces. 89 | * Added two new methods to `ICache`: `cacheAccessToken()` and `getAccessToken()`. 90 | * Optimized how the `LtiServiceConnector` caches access tokens to reduce the likelihood of being rate limited by Canvas. 91 | 92 | For upgrading from 1.0 to 2.0, view the [Upgrade Guide](UPGRADES.md) 93 | 94 | ## 1.1.1 95 | 96 | * Added a `text` parameter to `LtiDeepLinkResource` ([#5](https://github.com/packbackbooks/lti-1-3-php-library/pull/5)) 97 | 98 | ## 1.1.0 99 | 100 | * Added a custom Canvas extension to `LtiGrade` ([#3](https://github.com/packbackbooks/lti-1-3-php-library/pull/3)) 101 | 102 | ## 1.0.0 103 | 104 | * Initial release. View the [Laravel Implementation Guide](https://github.com/packbackbooks/lti-1-3-php-library/wiki/Laravel-Implementation-Guide). 105 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Build/Test/Lint Commands 6 | - Run all tests: `composer test` 7 | - Run a single test: `vendor/bin/phpunit tests/path/to/TestFile.php --filter testMethodName` 8 | - Run static analysis: `vendor/bin/phpstan analyse` 9 | - Check code style: `composer lint` 10 | - Fix code style issues: `composer lint-fix` 11 | 12 | ## Code Style Guidelines 13 | - Follow PSR-1/PSR-2 coding standards (Laravel preset) 14 | - Use PHP 8.1+ features and type hints 15 | - Classes organized in the namespace `Packback\Lti1p3` 16 | - Tests in the namespace `Tests` 17 | - Document public methods with PHPDoc blocks 18 | - Constants for error messages (use const ERR_* pattern) 19 | - Use interfaces for dependency injection 20 | - Error handling: Throw exceptions with descriptive messages 21 | - Import all classes with use statements 22 | - Use strict type checking (avoid loose comparisons) 23 | - Use camelCase for method names and variables 24 | - Line length: 80-120 characters -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary of Changes 2 | 3 | 4 | 5 | ## Testing 6 | 7 | 8 | 9 | - [ ] I have added automated tests for my changes 10 | - [ ] I ran `composer test` before opening this PR 11 | - [ ] I ran `composer lint-fix` before opening this PR 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LTI 1.3 Tool Library 2 | 3 | ![Test status](https://github.com/packbackbooks/lti-1-3-php-library/actions/workflows/run_tests.yml/badge.svg?branch=master) [![Maintainability](https://api.codeclimate.com/v1/badges/16055e83ea04ad95a2f9/maintainability)](https://codeclimate.com/github/packbackbooks/lti-1-3-php-library/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/16055e83ea04ad95a2f9/test_coverage)](https://codeclimate.com/github/packbackbooks/lti-1-3-php-library/test_coverage) 4 | 5 | A library used for building IMS-certified LTI 1.3 tool providers in PHP. 6 | 7 | This library allows a tool provider (your app) to receive LTI launches from a tool consumer (i.e. LMS). It validates LTI launches and lets an application interact with services like the Names Roles Provisioning Service (to fetch a roster for an LMS course) and Assignment Grades Service (to update grades for students in a course in the LMS). 8 | 9 | This library was forked from [IMSGlobal/lti-1-3-php-library](https://github.com/IMSGlobal/lti-1-3-php-library), initially created by @MartinLenord. [Packback](https://packback.co) found the library immensely helpful and extended it over the years. It has been rewritten by Packback to bring it into compliance with the standards set out by the PHP-FIG and the IMS LTI 1.3 Certification process. Packback actively uses and maintains this library. 10 | 11 | ## Installation 12 | 13 | Run: 14 | 15 | ```bash 16 | composer require packbackbooks/lti-1p3-tool 17 | ``` 18 | 19 | In your code, you will now be able to use classes in the `Packback\Lti1p3` namespace to access the library. 20 | 21 | ### Configure JWT 22 | 23 | Add the following when bootstrapping your app. 24 | 25 | ```php 26 | Firebase\JWT\JWT::$leeway = 5; 27 | ``` 28 | 29 | ### Implement Data Storage Interfaces 30 | 31 | This library uses three methods for storing and accessing data: cache, cookie, and database. All three must be implemented in order for the library to work. You may create your own custom implementations so long as they adhere to the following interfaces: 32 | 33 | - `Packback\Lti1p3\Interfaces\ICache` 34 | - `Packback\Lti1p3\Interfaces\ICookie` 35 | - `Packback\Lti1p3\Interfaces\IDatabase` or optionally `Packback\Lti1p3\Interfaces\IMigrationDatabase` 36 | 37 | View the [Laravel Implementation Guide](https://github.com/packbackbooks/lti-1-3-php-library/wiki/Laravel-Implementation-Guide) to see examples (or copy/paste the code outright). 38 | 39 | ### Create a JWKS endpoint 40 | 41 | A JWKS (JSON Web Key Set) endpoint can be generated for either an individual registration or from an array of `KID`s and private keys. 42 | 43 | ```php 44 | use Packback\Lti1p3\JwksEndpoint; 45 | 46 | // From issuer 47 | JwksEndpoint::fromIssuer($database, 'http://example.com')->getPublicJwks(); 48 | // From registration 49 | JwksEndpoint::fromRegistration($registration)->getPublicJwks(); 50 | // From array 51 | JwksEndpoint::new(['a_unique_KID' => file_get_contents('/path/to/private/key.pem')])->getPublicJwks(); 52 | ``` 53 | 54 | ## Documentation 55 | 56 | [The wiki](https://github.com/packbackbooks/lti-1-3-php-library/wiki) provides more detailed information about how to use this library, including a [Laravel Implementation Guide](https://github.com/packbackbooks/lti-1-3-php-library/wiki/Laravel-Implementation-Guide). 57 | 58 | ## Contributing 59 | 60 | For improvements, suggestions or bug fixes, make a pull request or an issue. Before opening a pull request, add automated tests for your changes, ensure that all tests pass, and any linting errors are fixed. 61 | 62 | ### Testing 63 | 64 | Automated tests can be run using the command: 65 | 66 | ```bash 67 | composer test 68 | ``` 69 | 70 | Linting can be run using 71 | 72 | ```bash 73 | # Display linting errors 74 | composer lint 75 | # Automatically fix linting errors 76 | composer lint-fix 77 | ``` 78 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | --------------------------------- | 7 | | 6.0.x | :white_check_mark: Active support | 8 | | 5.7.x | :wrench: Security fixes only | 9 | | < 5.7 | :x: End of life | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | To disclose a security vulnerability, email [lti-1p3-tool@packback.co](mailto:lti-1p3-tool@packback.co). 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "packbackbooks/lti-1p3-tool", 3 | "type": "library", 4 | "description": "A library used for building IMS-certified LTI 1.3 tool providers in PHP.", 5 | "license": "Apache-2.0", 6 | "keywords": [ 7 | "lti" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Davo Hynds", 12 | "email": "davo@packback.co" 13 | }, 14 | { 15 | "name": "Eric Tendian", 16 | "email": "eric@packback.co" 17 | }, 18 | { 19 | "name": "Martin Lenord", 20 | "email": "ims.m@rtin.dev" 21 | } 22 | ], 23 | "require": { 24 | "php": "^8.1", 25 | "firebase/php-jwt": "^6.6", 26 | "guzzlehttp/guzzle": "^7.0", 27 | "phpseclib/phpseclib": "^3.0" 28 | }, 29 | "require-dev": { 30 | "mockery/mockery": "^1.4", 31 | "nesbot/carbon": "^2.43 || ^3.0", 32 | "laravel/pint": "^1.0", 33 | "phpstan/phpstan": "^2.0", 34 | "phpunit/phpunit": "^9.0|^10.0" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Packback\\Lti1p3\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Tests\\": "tests/" 44 | } 45 | }, 46 | "scripts": { 47 | "test": "phpunit", 48 | "lint": [ 49 | "pint --test", 50 | "phpstan analyse" 51 | ], 52 | "lint-fix": "pint -v" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - src 5 | - tests 6 | 7 | ignoreErrors: 8 | - 9 | message: "#^Call to an undefined method Packback\\\\Lti1p3\\\\Interfaces\\\\IDatabase\\:\\:[a-zA-Z0-9]+\\(\\)\\.$#" 10 | count: 3 11 | path: src/LtiMessageLaunch.php 12 | 13 | - 14 | message: "#Call to an undefined method Mockery\\\\#" 15 | paths: 16 | - tests/* 17 | 18 | - 19 | message: "# Mockery\\\\(Legacy)*MockInterface given\\.$#" 20 | paths: 21 | - tests/* 22 | 23 | - 24 | message: "#^Result of static method Packback\\\\Lti1p3\\\\MessageValidators\\\\[A-Za-z]+MessageValidator\\:\\:validate\\(\\) \\(void\\) is used\\.$#" 25 | path: tests/MessageValidators/* 26 | 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | tests 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | src/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "cache-file": ".pint.cache", 4 | "rules": { 5 | "not_operator_with_successor_space": false, 6 | "class_attributes_separation": { 7 | "elements": { 8 | "const": "only_if_meta", 9 | "property": "only_if_meta" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Concerns/Arrayable.php: -------------------------------------------------------------------------------- 1 | getArray()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Concerns/JsonStringable.php: -------------------------------------------------------------------------------- 1 | toArray()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/DeepLinkResources/DateTimeInterval.php: -------------------------------------------------------------------------------- 1 | validateStartAndEnd(); 20 | } 21 | 22 | public static function new(): self 23 | { 24 | return new DateTimeInterval; 25 | } 26 | 27 | public function getArray(): array 28 | { 29 | if (!isset($this->start) && !isset($this->end)) { 30 | throw new LtiException(self::ERROR_NO_START_OR_END); 31 | } 32 | 33 | $this->validateStartAndEnd(); 34 | 35 | return [ 36 | 'startDateTime' => $this->start?->format(DateTime::ATOM), 37 | 'endDateTime' => $this->end?->format(DateTime::ATOM), 38 | ]; 39 | } 40 | 41 | public function setStart(?DateTime $start): self 42 | { 43 | $this->start = $start; 44 | 45 | return $this; 46 | } 47 | 48 | public function getStart(): ?DateTime 49 | { 50 | return $this->start; 51 | } 52 | 53 | public function setEnd(?DateTime $end): self 54 | { 55 | $this->end = $end; 56 | 57 | return $this; 58 | } 59 | 60 | public function getEnd(): ?DateTime 61 | { 62 | return $this->end; 63 | } 64 | 65 | /** 66 | * @throws LtiException 67 | */ 68 | private function validateStartAndEnd(): void 69 | { 70 | if (isset($this->start) && isset($this->end) && $this->start > $this->end) { 71 | throw new LtiException(self::ERROR_START_GT_END); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/DeepLinkResources/HasDimensions.php: -------------------------------------------------------------------------------- 1 | width = $width; 10 | 11 | return $this; 12 | } 13 | 14 | public function getWidth(): ?int 15 | { 16 | return $this->width; 17 | } 18 | 19 | public function setHeight(?int $height): self 20 | { 21 | $this->height = $height; 22 | 23 | return $this; 24 | } 25 | 26 | public function getHeight(): ?int 27 | { 28 | return $this->height; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DeepLinkResources/Icon.php: -------------------------------------------------------------------------------- 1 | $this->url, 26 | 'width' => $this->width, 27 | 'height' => $this->height, 28 | ]; 29 | } 30 | 31 | public function setUrl(string $url): self 32 | { 33 | $this->url = $url; 34 | 35 | return $this; 36 | } 37 | 38 | public function getUrl(): string 39 | { 40 | return $this->url; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/DeepLinkResources/Iframe.php: -------------------------------------------------------------------------------- 1 | $this->width, 26 | 'height' => $this->height, 27 | 'src' => $this->src, 28 | ]; 29 | } 30 | 31 | public function setSrc(?string $src): self 32 | { 33 | $this->src = $src; 34 | 35 | return $this; 36 | } 37 | 38 | public function getSrc(): ?string 39 | { 40 | return $this->src; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/DeepLinkResources/Resource.php: -------------------------------------------------------------------------------- 1 | $this->type, 35 | 'title' => $this->title, 36 | 'text' => $this->text, 37 | 'url' => $this->url, 38 | 'icon' => $this->icon?->toArray(), 39 | 'thumbnail' => $this->thumbnail?->toArray(), 40 | 'iframe' => $this->iframe?->toArray(), 41 | 'window' => $this->window?->toArray(), 42 | 'available' => $this->availability_interval?->toArray(), 43 | 'submission' => $this->submission_interval?->toArray(), 44 | ]; 45 | 46 | if (!empty($this->custom_params)) { 47 | $resource['custom'] = $this->custom_params; 48 | } 49 | 50 | if (isset($this->line_item)) { 51 | $resource['lineItem'] = [ 52 | 'scoreMaximum' => $this->line_item->getScoreMaximum(), 53 | 'label' => $this->line_item->getLabel(), 54 | 'resourceId' => $this->line_item->getResourceId(), 55 | 'tag' => $this->line_item->getTag(), 56 | ]; 57 | } 58 | 59 | return $resource; 60 | } 61 | 62 | public function getType(): string 63 | { 64 | return $this->type; 65 | } 66 | 67 | public function setType(string $value): self 68 | { 69 | $this->type = $value; 70 | 71 | return $this; 72 | } 73 | 74 | public function getTitle(): ?string 75 | { 76 | return $this->title; 77 | } 78 | 79 | public function setTitle(?string $value): self 80 | { 81 | $this->title = $value; 82 | 83 | return $this; 84 | } 85 | 86 | public function getText(): ?string 87 | { 88 | return $this->text; 89 | } 90 | 91 | public function setText(?string $value): self 92 | { 93 | $this->text = $value; 94 | 95 | return $this; 96 | } 97 | 98 | public function getUrl(): ?string 99 | { 100 | return $this->url; 101 | } 102 | 103 | public function setUrl(?string $value): self 104 | { 105 | $this->url = $value; 106 | 107 | return $this; 108 | } 109 | 110 | public function getLineItem(): ?LtiLineitem 111 | { 112 | return $this->line_item; 113 | } 114 | 115 | public function setLineItem(?LtiLineitem $value): self 116 | { 117 | $this->line_item = $value; 118 | 119 | return $this; 120 | } 121 | 122 | public function setIcon(?Icon $icon): self 123 | { 124 | $this->icon = $icon; 125 | 126 | return $this; 127 | } 128 | 129 | public function getIcon(): ?Icon 130 | { 131 | return $this->icon; 132 | } 133 | 134 | public function setThumbnail(?Icon $thumbnail): self 135 | { 136 | $this->thumbnail = $thumbnail; 137 | 138 | return $this; 139 | } 140 | 141 | public function getThumbnail(): ?Icon 142 | { 143 | return $this->thumbnail; 144 | } 145 | 146 | public function getCustomParams(): array 147 | { 148 | return $this->custom_params; 149 | } 150 | 151 | public function setCustomParams(array $value): self 152 | { 153 | $this->custom_params = $value; 154 | 155 | return $this; 156 | } 157 | 158 | public function getIframe(): ?Iframe 159 | { 160 | return $this->iframe; 161 | } 162 | 163 | public function setIframe(?Iframe $iframe): self 164 | { 165 | $this->iframe = $iframe; 166 | 167 | return $this; 168 | } 169 | 170 | public function getWindow(): ?Window 171 | { 172 | return $this->window; 173 | } 174 | 175 | public function setWindow(?Window $window): self 176 | { 177 | $this->window = $window; 178 | 179 | return $this; 180 | } 181 | 182 | public function getAvailabilityInterval(): ?DateTimeInterval 183 | { 184 | return $this->availability_interval; 185 | } 186 | 187 | public function setAvailabilityInterval(?DateTimeInterval $availabilityInterval): self 188 | { 189 | $this->availability_interval = $availabilityInterval; 190 | 191 | return $this; 192 | } 193 | 194 | public function getSubmissionInterval(): ?DateTimeInterval 195 | { 196 | return $this->submission_interval; 197 | } 198 | 199 | public function setSubmissionInterval(?DateTimeInterval $submissionInterval): self 200 | { 201 | $this->submission_interval = $submissionInterval; 202 | 203 | return $this; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/DeepLinkResources/Window.php: -------------------------------------------------------------------------------- 1 | $this->target_name, 27 | 'width' => $this->width, 28 | 'height' => $this->height, 29 | 'windowFeatures' => $this->window_features, 30 | ]; 31 | } 32 | 33 | public function setTargetName(?string $targetName): self 34 | { 35 | $this->target_name = $targetName; 36 | 37 | return $this; 38 | } 39 | 40 | public function getTargetName(): ?string 41 | { 42 | return $this->target_name; 43 | } 44 | 45 | public function setWindowFeatures(?string $windowFeatures): self 46 | { 47 | $this->window_features = $windowFeatures; 48 | 49 | return $this; 50 | } 51 | 52 | public function getWindowFeatures(): ?string 53 | { 54 | return $this->window_features; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Helpers/Helpers.php: -------------------------------------------------------------------------------- 1 | !is_null($value)); 10 | } 11 | 12 | public static function buildUrlWithQueryParams(string $url, array $params = []): string 13 | { 14 | if (empty($params)) { 15 | return $url; 16 | } 17 | 18 | if (parse_url($url, PHP_URL_QUERY)) { 19 | $separator = '&'; 20 | } else { 21 | $separator = '?'; 22 | } 23 | 24 | return $url.$separator.http_build_query($params, ''); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Interfaces/ICache.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function findLti1p1Keys(LtiMessageLaunch $launch): array; 22 | 23 | /** 24 | * Given an LtiMessageLaunch, return true if this tool should migrate from 1.1 to 1.3 25 | */ 26 | public function shouldMigrate(LtiMessageLaunch $launch): bool; 27 | 28 | /** 29 | * This method should create a 1.3 deployment in your DB based on the LtiMessageLaunch. 30 | * Previous to this, we validated the oauth_consumer_key_sign to ensure this migration 31 | * can safely occur. 32 | */ 33 | public function migrateFromLti1p1(LtiMessageLaunch $launch): ?ILtiDeployment; 34 | } 35 | -------------------------------------------------------------------------------- /src/Interfaces/IServiceRequest.php: -------------------------------------------------------------------------------- 1 | findRegistrationByIssuer($issuer); 21 | 22 | return new JwksEndpoint([$registration->getKid() => $registration->getToolPrivateKey()]); 23 | } 24 | 25 | public static function fromRegistration(ILtiRegistration $registration): self 26 | { 27 | return new JwksEndpoint([$registration->getKid() => $registration->getToolPrivateKey()]); 28 | } 29 | 30 | public function getPublicJwks(): array 31 | { 32 | $jwks = []; 33 | foreach ($this->keys as $kid => $private_key) { 34 | $key = RSA::load($private_key); 35 | $jwk = json_decode($key->getPublicKey()->toString('JWK'), true); 36 | $jwks[] = array_merge($jwk['keys'][0], [ 37 | 'alg' => 'RS256', 38 | 'use' => 'sig', 39 | 'kid' => $kid, 40 | ]); 41 | } 42 | 43 | return ['keys' => $jwks]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Lti1p1Key.php: -------------------------------------------------------------------------------- 1 | key = $key['key'] ?? null; 18 | $this->secret = $key['secret'] ?? null; 19 | } 20 | 21 | public function getKey(): ?string 22 | { 23 | return $this->key; 24 | } 25 | 26 | public function setKey(string $key): self 27 | { 28 | $this->key = $key; 29 | 30 | return $this; 31 | } 32 | 33 | public function getSecret(): ?string 34 | { 35 | return $this->secret; 36 | } 37 | 38 | public function setSecret(string $secret): self 39 | { 40 | $this->secret = $secret; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Create a signature using the key and secret 47 | * 48 | * @see https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign 49 | */ 50 | public function sign(string $deploymentId, string $iss, string $clientId, string $exp, string $nonce): string 51 | { 52 | $signatureComponents = [ 53 | $this->getKey(), 54 | $deploymentId, 55 | $iss, 56 | $clientId, 57 | $exp, 58 | $nonce, 59 | ]; 60 | 61 | $baseString = implode('&', $signatureComponents); 62 | $utf8String = mb_convert_encoding($baseString, 'utf8', mb_detect_encoding($baseString)); 63 | $hash = hash_hmac('sha256', $utf8String, $this->getSecret(), true); 64 | 65 | return base64_encode($hash); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/LtiAbstractService.php: -------------------------------------------------------------------------------- 1 | serviceData; 20 | } 21 | 22 | public function setServiceData(array $serviceData): self 23 | { 24 | $this->serviceData = $serviceData; 25 | 26 | return $this; 27 | } 28 | 29 | abstract public function getScope(): array; 30 | 31 | protected function validateScopes(array $scopes): void 32 | { 33 | if (empty(array_intersect($scopes, $this->getScope()))) { 34 | throw new LtiException('Missing required scope'); 35 | } 36 | } 37 | 38 | protected function makeServiceRequest(IServiceRequest $request): array 39 | { 40 | return $this->serviceConnector->makeServiceRequest( 41 | $this->registration, 42 | $this->getScope(), 43 | $request, 44 | ); 45 | } 46 | 47 | protected function getAll(IServiceRequest $request, ?string $key = null): array 48 | { 49 | return $this->serviceConnector->getAll( 50 | $this->registration, 51 | $this->getScope(), 52 | $request, 53 | $key 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/LtiCourseGroupsService.php: -------------------------------------------------------------------------------- 1 | getServiceData()['scope']; 12 | } 13 | 14 | public function getGroups(): array 15 | { 16 | $request = new ServiceRequest( 17 | ServiceRequest::METHOD_GET, 18 | $this->getServiceData()['context_groups_url'], 19 | ServiceRequest::TYPE_GET_GROUPS 20 | ); 21 | $request->setAccept(static::CONTENTTYPE_CONTEXTGROUPCONTAINER); 22 | 23 | return $this->getAll($request, 'groups'); 24 | } 25 | 26 | public function getSets(): array 27 | { 28 | // Sets are optional. 29 | if (!isset($this->getServiceData()['context_group_sets_url'])) { 30 | return []; 31 | } 32 | 33 | $request = new ServiceRequest( 34 | ServiceRequest::METHOD_GET, 35 | $this->getServiceData()['context_group_sets_url'], 36 | ServiceRequest::TYPE_GET_SETS 37 | ); 38 | $request->setAccept(static::CONTENTTYPE_CONTEXTGROUPCONTAINER); 39 | 40 | return $this->getAll($request, 'sets'); 41 | } 42 | 43 | public function getGroupsBySet(): array 44 | { 45 | $groups = $this->getGroups(); 46 | $sets = $this->getSets(); 47 | 48 | $groupsBySet = []; 49 | $unsetted = []; 50 | 51 | foreach ($sets as $key => $set) { 52 | $groupsBySet[$set['id']] = $set; 53 | $groupsBySet[$set['id']]['groups'] = []; 54 | } 55 | 56 | foreach ($groups as $key => $group) { 57 | if (isset($group['set_id']) && isset($groupsBySet[$group['set_id']])) { 58 | $groupsBySet[$group['set_id']]['groups'][$group['id']] = $group; 59 | } else { 60 | $unsetted[$group['id']] = $group; 61 | } 62 | } 63 | 64 | if (!empty($unsetted)) { 65 | $groupsBySet['none'] = [ 66 | 'name' => 'None', 67 | 'id' => 'none', 68 | 'groups' => $unsetted, 69 | ]; 70 | } 71 | 72 | return $groupsBySet; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/LtiDeepLink.php: -------------------------------------------------------------------------------- 1 | $this->registration->getClientId(), 20 | 'aud' => [$this->registration->getIssuer()], 21 | 'exp' => time() + 600, 22 | 'iat' => time(), 23 | 'nonce' => LtiOidcLogin::secureRandomString('nonce-'), 24 | LtiConstants::DEPLOYMENT_ID => $this->deployment_id, 25 | LtiConstants::MESSAGE_TYPE => LtiConstants::MESSAGE_TYPE_DEEPLINK_RESPONSE, 26 | LtiConstants::VERSION => LtiConstants::V1_3, 27 | LtiConstants::DL_CONTENT_ITEMS => array_map(function ($resource) { 28 | return $resource->toArray(); 29 | }, $resources), 30 | ]; 31 | 32 | // https://www.imsglobal.org/spec/lti-dl/v2p0/#deep-linking-request-message 33 | // 'data' is an optional property which, if it exists, must be returned by the tool 34 | if (isset($this->settings()['data'])) { 35 | $message_jwt[LtiConstants::DL_DATA] = $this->settings()['data']; 36 | } 37 | 38 | return JWT::encode($message_jwt, $this->registration->getToolPrivateKey(), 'RS256', $this->registration->getKid()); 39 | } 40 | 41 | public function settings(): array 42 | { 43 | return $this->deep_link_settings; 44 | } 45 | 46 | public function returnUrl(): string 47 | { 48 | return $this->settings()['deep_link_return_url']; 49 | } 50 | 51 | public function acceptTypes(): array 52 | { 53 | return $this->settings()['accept_types']; 54 | } 55 | 56 | public function canAcceptType(string $acceptType): bool 57 | { 58 | return in_array($acceptType, $this->acceptTypes()); 59 | } 60 | 61 | public function acceptPresentationDocumentTargets(): array 62 | { 63 | return $this->settings()['accept_presentation_document_targets']; 64 | } 65 | 66 | public function canAcceptPresentationDocumentTarget(string $target): bool 67 | { 68 | return in_array($target, $this->acceptPresentationDocumentTargets()); 69 | } 70 | 71 | public function acceptMediaTypes(): ?string 72 | { 73 | return $this->settings()['accept_media_types'] ?? null; 74 | } 75 | 76 | public function canAcceptMultiple(): bool 77 | { 78 | return $this->settings()['accept_multiple'] ?? false; 79 | } 80 | 81 | public function canAcceptLineitem(): bool 82 | { 83 | return $this->settings()['accept_lineitem'] ?? false; 84 | } 85 | 86 | public function canAutoCreate(): bool 87 | { 88 | return $this->settings()['auto_create'] ?? false; 89 | } 90 | 91 | public function title(): ?string 92 | { 93 | return $this->settings()['title'] ?? null; 94 | } 95 | 96 | public function text(): ?string 97 | { 98 | return $this->settings()['text'] ?? null; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/LtiDeployment.php: -------------------------------------------------------------------------------- 1 | deployment_id; 21 | } 22 | 23 | public function setDeploymentId($deployment_id): self 24 | { 25 | $this->deployment_id = $deployment_id; 26 | 27 | return $this; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/LtiException.php: -------------------------------------------------------------------------------- 1 | score_given = $grade['scoreGiven'] ?? null; 23 | $this->score_maximum = $grade['scoreMaximum'] ?? null; 24 | $this->comment = $grade['comment'] ?? null; 25 | $this->activity_progress = $grade['activityProgress'] ?? null; 26 | $this->grading_progress = $grade['gradingProgress'] ?? null; 27 | $this->timestamp = $grade['timestamp'] ?? null; 28 | $this->user_id = $grade['userId'] ?? null; 29 | $this->submission_review = $grade['submissionReview'] ?? null; 30 | $this->canvas_extension = $grade['https://canvas.instructure.com/lti/submission'] ?? null; 31 | } 32 | 33 | public function getArray(): array 34 | { 35 | return [ 36 | 'scoreGiven' => $this->score_given, 37 | 'scoreMaximum' => $this->score_maximum, 38 | 'comment' => $this->comment, 39 | 'activityProgress' => $this->activity_progress, 40 | 'gradingProgress' => $this->grading_progress, 41 | 'timestamp' => $this->timestamp, 42 | 'userId' => $this->user_id, 43 | 'submissionReview' => $this->submission_review, 44 | 'https://canvas.instructure.com/lti/submission' => $this->canvas_extension, 45 | ]; 46 | } 47 | 48 | /** 49 | * Static function to allow for method chaining without having to assign to a variable first. 50 | */ 51 | public static function new(): self 52 | { 53 | return new LtiGrade; 54 | } 55 | 56 | public function getScoreGiven() 57 | { 58 | return $this->score_given; 59 | } 60 | 61 | public function setScoreGiven($value): self 62 | { 63 | $this->score_given = $value; 64 | 65 | return $this; 66 | } 67 | 68 | public function getScoreMaximum() 69 | { 70 | return $this->score_maximum; 71 | } 72 | 73 | public function setScoreMaximum($value): self 74 | { 75 | $this->score_maximum = $value; 76 | 77 | return $this; 78 | } 79 | 80 | public function getComment() 81 | { 82 | return $this->comment; 83 | } 84 | 85 | public function setComment($comment): self 86 | { 87 | $this->comment = $comment; 88 | 89 | return $this; 90 | } 91 | 92 | public function getActivityProgress() 93 | { 94 | return $this->activity_progress; 95 | } 96 | 97 | public function setActivityProgress($value): self 98 | { 99 | $this->activity_progress = $value; 100 | 101 | return $this; 102 | } 103 | 104 | public function getGradingProgress() 105 | { 106 | return $this->grading_progress; 107 | } 108 | 109 | public function setGradingProgress($value): self 110 | { 111 | $this->grading_progress = $value; 112 | 113 | return $this; 114 | } 115 | 116 | public function getTimestamp() 117 | { 118 | return $this->timestamp; 119 | } 120 | 121 | public function setTimestamp($value): self 122 | { 123 | $this->timestamp = $value; 124 | 125 | return $this; 126 | } 127 | 128 | public function getUserId() 129 | { 130 | return $this->user_id; 131 | } 132 | 133 | public function setUserId($value): self 134 | { 135 | $this->user_id = $value; 136 | 137 | return $this; 138 | } 139 | 140 | public function getSubmissionReview() 141 | { 142 | return $this->submission_review; 143 | } 144 | 145 | public function setSubmissionReview($value): self 146 | { 147 | $this->submission_review = $value; 148 | 149 | return $this; 150 | } 151 | 152 | public function getCanvasExtension() 153 | { 154 | return $this->canvas_extension; 155 | } 156 | 157 | /** 158 | * Add custom extensions for Canvas. 159 | * 160 | * Disclaimer: You should only set this if your LMS is Canvas. 161 | * Some LMS (e.g. Schoology) include validation logic that will throw if there 162 | * is unexpected data. And, the type of LMS cannot simply be inferred by their URL. 163 | * 164 | * @see https://documentation.instructure.com/doc/api/score.html 165 | */ 166 | public function setCanvasExtension($value): self 167 | { 168 | $this->canvas_extension = $value; 169 | 170 | return $this; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/LtiGradeSubmissionReview.php: -------------------------------------------------------------------------------- 1 | reviewable_status = $gradeSubmission['reviewableStatus'] ?? null; 18 | $this->label = $gradeSubmission['label'] ?? null; 19 | $this->url = $gradeSubmission['url'] ?? null; 20 | $this->custom = $gradeSubmission['custom'] ?? null; 21 | } 22 | 23 | public function getArray(): array 24 | { 25 | return [ 26 | 'reviewableStatus' => $this->reviewable_status, 27 | 'label' => $this->label, 28 | 'url' => $this->url, 29 | 'custom' => $this->custom, 30 | ]; 31 | } 32 | 33 | /** 34 | * Static function to allow for method chaining without having to assign to a variable first. 35 | */ 36 | public static function new(): self 37 | { 38 | return new LtiGradeSubmissionReview; 39 | } 40 | 41 | public function getReviewableStatus() 42 | { 43 | return $this->reviewable_status; 44 | } 45 | 46 | public function setReviewableStatus($value): self 47 | { 48 | $this->reviewable_status = $value; 49 | 50 | return $this; 51 | } 52 | 53 | public function getLabel() 54 | { 55 | return $this->label; 56 | } 57 | 58 | public function setLabel($value): self 59 | { 60 | $this->label = $value; 61 | 62 | return $this; 63 | } 64 | 65 | public function getUrl() 66 | { 67 | return $this->url; 68 | } 69 | 70 | public function setUrl($url): self 71 | { 72 | $this->url = $url; 73 | 74 | return $this; 75 | } 76 | 77 | public function getCustom() 78 | { 79 | return $this->custom; 80 | } 81 | 82 | public function setCustom($value): self 83 | { 84 | $this->custom = $value; 85 | 86 | return $this; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/LtiLineitem.php: -------------------------------------------------------------------------------- 1 | id = $lineitem['id'] ?? null; 23 | $this->score_maximum = $lineitem['scoreMaximum'] ?? null; 24 | $this->label = $lineitem['label'] ?? null; 25 | $this->resource_id = $lineitem['resourceId'] ?? null; 26 | $this->resource_link_id = $lineitem['resourceLinkId'] ?? null; 27 | $this->tag = $lineitem['tag'] ?? null; 28 | $this->start_date_time = $lineitem['startDateTime'] ?? null; 29 | $this->end_date_time = $lineitem['endDateTime'] ?? null; 30 | $this->grades_released = $lineitem['gradesReleased'] ?? null; 31 | } 32 | 33 | /** 34 | * Static function to allow for method chaining without having to assign to a variable first. 35 | */ 36 | public static function new(?array $lineItem = null): self 37 | { 38 | return new LtiLineitem($lineItem); 39 | } 40 | 41 | public function getArray(): array 42 | { 43 | return [ 44 | 'id' => $this->id, 45 | 'scoreMaximum' => $this->score_maximum, 46 | 'label' => $this->label, 47 | 'resourceId' => $this->resource_id, 48 | 'resourceLinkId' => $this->resource_link_id, 49 | 'tag' => $this->tag, 50 | 'startDateTime' => $this->start_date_time, 51 | 'endDateTime' => $this->end_date_time, 52 | 'gradesReleased' => $this->grades_released, 53 | ]; 54 | } 55 | 56 | public function getId() 57 | { 58 | return $this->id; 59 | } 60 | 61 | public function setId($value): self 62 | { 63 | $this->id = $value; 64 | 65 | return $this; 66 | } 67 | 68 | public function getLabel() 69 | { 70 | return $this->label; 71 | } 72 | 73 | public function setLabel($value): self 74 | { 75 | $this->label = $value; 76 | 77 | return $this; 78 | } 79 | 80 | public function getScoreMaximum() 81 | { 82 | return $this->score_maximum; 83 | } 84 | 85 | public function setScoreMaximum($value): self 86 | { 87 | $this->score_maximum = $value; 88 | 89 | return $this; 90 | } 91 | 92 | public function getResourceId() 93 | { 94 | return $this->resource_id; 95 | } 96 | 97 | public function setResourceId($value): self 98 | { 99 | $this->resource_id = $value; 100 | 101 | return $this; 102 | } 103 | 104 | public function getResourceLinkId() 105 | { 106 | return $this->resource_link_id; 107 | } 108 | 109 | public function setResourceLinkId($value): self 110 | { 111 | $this->resource_link_id = $value; 112 | 113 | return $this; 114 | } 115 | 116 | public function getTag() 117 | { 118 | return $this->tag; 119 | } 120 | 121 | public function setTag($value): self 122 | { 123 | $this->tag = $value; 124 | 125 | return $this; 126 | } 127 | 128 | public function getStartDateTime() 129 | { 130 | return $this->start_date_time; 131 | } 132 | 133 | public function setStartDateTime($value): self 134 | { 135 | $this->start_date_time = $value; 136 | 137 | return $this; 138 | } 139 | 140 | public function getEndDateTime() 141 | { 142 | return $this->end_date_time; 143 | } 144 | 145 | public function setEndDateTime($value): self 146 | { 147 | $this->end_date_time = $value; 148 | 149 | return $this; 150 | } 151 | 152 | public function getGradesReleased(): ?bool 153 | { 154 | return $this->grades_released; 155 | } 156 | 157 | public function setGradesReleased(?bool $value): self 158 | { 159 | $this->grades_released = $value; 160 | 161 | return $this; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/LtiNamesRolesProvisioningService.php: -------------------------------------------------------------------------------- 1 | getServiceData()['context_memberships_url'], $options); 22 | 23 | $request = new ServiceRequest( 24 | ServiceRequest::METHOD_GET, 25 | $url, 26 | ServiceRequest::TYPE_GET_MEMBERSHIPS 27 | ); 28 | $request->setAccept(static::CONTENTTYPE_MEMBERSHIPCONTAINER); 29 | 30 | return $this->getAll($request, 'members'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/LtiOidcLogin.php: -------------------------------------------------------------------------------- 1 | validateOidcLogin($request); 39 | 40 | // Build OIDC Auth response. 41 | $authParams = $this->getAuthParams($launchUrl, $registration->getClientId(), $request); 42 | 43 | return Helpers::buildUrlWithQueryParams($registration->getAuthLoginUrl(), $authParams); 44 | } 45 | 46 | public function validateOidcLogin(array $request): ILtiRegistration 47 | { 48 | if (!isset($request['iss'])) { 49 | throw new OidcException(static::ERROR_MSG_ISSUER); 50 | } 51 | 52 | if (!isset($request['login_hint'])) { 53 | throw new OidcException(static::ERROR_MSG_LOGIN_HINT); 54 | } 55 | 56 | // Fetch registration 57 | $clientId = $request['client_id'] ?? null; 58 | $registration = $this->db->findRegistrationByIssuer($request['iss'], $clientId); 59 | 60 | if (!isset($registration)) { 61 | $errorMsg = LtiMessageLaunch::getMissingRegistrationErrorMsg($request['iss'], $clientId); 62 | 63 | throw new OidcException($errorMsg); 64 | } 65 | 66 | return $registration; 67 | } 68 | 69 | public function getAuthParams(string $launchUrl, string $clientId, array $request): array 70 | { 71 | // Set cookie (short lived) 72 | $state = static::secureRandomString('state-'); 73 | $this->cookie->setCookie(static::COOKIE_PREFIX.$state, $state, 60); 74 | 75 | $nonce = static::secureRandomString('nonce-'); 76 | $this->cache->cacheNonce($nonce, $state); 77 | 78 | $authParams = [ 79 | 'scope' => 'openid', // OIDC Scope. 80 | 'response_type' => 'id_token', // OIDC response is always an id token. 81 | 'response_mode' => 'form_post', // OIDC response is always a form post. 82 | 'prompt' => 'none', // Don't prompt user on redirect. 83 | 'client_id' => $clientId, // Registered client id. 84 | 'redirect_uri' => $launchUrl, // URL to return to after login. 85 | 'state' => $state, // State to identify browser session. 86 | 'nonce' => $nonce, // Prevent replay attacks. 87 | 'login_hint' => $request['login_hint'], // Login hint to identify platform session. 88 | ]; 89 | 90 | if (isset($request['lti_message_hint'])) { 91 | // LTI message hint to identify LTI context within the platform. 92 | $authParams['lti_message_hint'] = $request['lti_message_hint']; 93 | } 94 | 95 | return $authParams; 96 | } 97 | 98 | public static function secureRandomString(string $prefix = ''): string 99 | { 100 | return $prefix.hash('sha256', random_bytes(64)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/LtiRegistration.php: -------------------------------------------------------------------------------- 1 | issuer = $registration['issuer'] ?? null; 21 | $this->clientId = $registration['clientId'] ?? null; 22 | $this->keySetUrl = $registration['keySetUrl'] ?? null; 23 | $this->authTokenUrl = $registration['authTokenUrl'] ?? null; 24 | $this->authLoginUrl = $registration['authLoginUrl'] ?? null; 25 | $this->authServer = $registration['authServer'] ?? null; 26 | $this->toolPrivateKey = $registration['toolPrivateKey'] ?? null; 27 | $this->kid = $registration['kid'] ?? null; 28 | } 29 | 30 | public static function new(?array $registration = null): self 31 | { 32 | return new LtiRegistration($registration); 33 | } 34 | 35 | public function getIssuer() 36 | { 37 | return $this->issuer; 38 | } 39 | 40 | public function setIssuer(string $issuer): self 41 | { 42 | $this->issuer = $issuer; 43 | 44 | return $this; 45 | } 46 | 47 | public function getClientId() 48 | { 49 | return $this->clientId; 50 | } 51 | 52 | public function setClientId(string $clientId): self 53 | { 54 | $this->clientId = $clientId; 55 | 56 | return $this; 57 | } 58 | 59 | public function getKeySetUrl(): ?string 60 | { 61 | return $this->keySetUrl; 62 | } 63 | 64 | public function setKeySetUrl(?string $keySetUrl): self 65 | { 66 | $this->keySetUrl = $keySetUrl; 67 | 68 | return $this; 69 | } 70 | 71 | public function getAuthTokenUrl(): ?string 72 | { 73 | return $this->authTokenUrl; 74 | } 75 | 76 | public function setAuthTokenUrl(?string $authTokenUrl): self 77 | { 78 | $this->authTokenUrl = $authTokenUrl; 79 | 80 | return $this; 81 | } 82 | 83 | public function getAuthLoginUrl(): ?string 84 | { 85 | return $this->authLoginUrl; 86 | } 87 | 88 | public function setAuthLoginUrl(?string $authLoginUrl): self 89 | { 90 | $this->authLoginUrl = $authLoginUrl; 91 | 92 | return $this; 93 | } 94 | 95 | public function getAuthServer(): ?string 96 | { 97 | return $this->authServer ?? $this->authTokenUrl; 98 | } 99 | 100 | public function setAuthServer(?string $authServer): self 101 | { 102 | $this->authServer = $authServer; 103 | 104 | return $this; 105 | } 106 | 107 | public function getToolPrivateKey() 108 | { 109 | return $this->toolPrivateKey; 110 | } 111 | 112 | public function setToolPrivateKey(string $toolPrivateKey): self 113 | { 114 | $this->toolPrivateKey = $toolPrivateKey; 115 | 116 | return $this; 117 | } 118 | 119 | public function getKid() 120 | { 121 | return $this->kid ?? hash('sha256', trim($this->issuer.$this->clientId)); 122 | } 123 | 124 | public function setKid(string $kid): self 125 | { 126 | $this->kid = $kid; 127 | 128 | return $this; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/MessageValidators/AbstractMessageValidator.php: -------------------------------------------------------------------------------- 1 | method); 55 | } 56 | 57 | public function getUrl(): string 58 | { 59 | return $this->url; 60 | } 61 | 62 | public function getPayload(): array 63 | { 64 | if (isset($this->payload)) { 65 | return $this->payload; 66 | } 67 | 68 | $payload = [ 69 | 'headers' => $this->getHeaders(), 70 | ]; 71 | 72 | $body = $this->getBody(); 73 | if ($body) { 74 | $payload['body'] = $body; 75 | } 76 | 77 | return $payload; 78 | } 79 | 80 | public function setUrl(string $url): IServiceRequest 81 | { 82 | $this->url = $url; 83 | 84 | return $this; 85 | } 86 | 87 | public function setAccessToken(string $accessToken): IServiceRequest 88 | { 89 | $this->accessToken = 'Bearer '.$accessToken; 90 | 91 | return $this; 92 | } 93 | 94 | public function setBody(string $body): IServiceRequest 95 | { 96 | $this->body = $body; 97 | 98 | return $this; 99 | } 100 | 101 | public function setPayload(array $payload): IServiceRequest 102 | { 103 | $this->payload = $payload; 104 | 105 | return $this; 106 | } 107 | 108 | public function setAccept(string $accept): IServiceRequest 109 | { 110 | $this->accept = $accept; 111 | 112 | return $this; 113 | } 114 | 115 | public function setContentType(string $contentType): IServiceRequest 116 | { 117 | $this->contentType = $contentType; 118 | 119 | return $this; 120 | } 121 | 122 | public function getMaskResponseLogs(): bool 123 | { 124 | return $this->maskResponseLogs; 125 | } 126 | 127 | public function setMaskResponseLogs(bool $shouldMask): IServiceRequest 128 | { 129 | $this->maskResponseLogs = $shouldMask; 130 | 131 | return $this; 132 | } 133 | 134 | public function getErrorPrefix(): string 135 | { 136 | $defaultMessage = 'Logging request data:'; 137 | $errorMessages = [ 138 | static::TYPE_UNSUPPORTED => $defaultMessage, 139 | static::TYPE_AUTH => 'Authenticating:', 140 | static::TYPE_GET_KEYSET => 'Getting key set:', 141 | static::TYPE_GET_GRADES => 'Getting grades:', 142 | static::TYPE_SYNC_GRADE => 'Syncing grade for this lti_user_id:', 143 | static::TYPE_CREATE_LINEITEM => 'Creating lineitem:', 144 | static::TYPE_DELETE_LINEITEM => 'Deleting lineitem:', 145 | static::TYPE_GET_LINEITEMS => 'Getting lineitems:', 146 | static::TYPE_GET_LINEITEM => 'Getting a lineitem:', 147 | static::TYPE_UPDATE_LINEITEM => 'Updating lineitem:', 148 | static::TYPE_GET_GROUPS => 'Getting groups:', 149 | static::TYPE_GET_SETS => 'Getting sets:', 150 | static::TYPE_GET_MEMBERSHIPS => 'Getting memberships:', 151 | ]; 152 | 153 | return $errorMessages[$this->type] ?? $defaultMessage; 154 | } 155 | 156 | private function getHeaders(): array 157 | { 158 | $headers = [ 159 | 'Accept' => $this->accept, 160 | ]; 161 | 162 | if (isset($this->accessToken)) { 163 | $headers['Authorization'] = $this->accessToken; 164 | } 165 | 166 | // Include Content-Type for POST and PUT requests 167 | if (in_array($this->getMethod(), [ServiceRequest::METHOD_POST, ServiceRequest::METHOD_PUT])) { 168 | $headers['Content-Type'] = $this->contentType; 169 | } 170 | 171 | return $headers; 172 | } 173 | 174 | private function getBody(): ?string 175 | { 176 | return $this->body; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/DeepLinkResources/DateTimeIntervalTest.php: -------------------------------------------------------------------------------- 1 | initialStart = date_create(); 19 | $this->initialEnd = date_create(); 20 | $this->dateTimeInterval = new DateTimeInterval($this->initialStart, $this->initialEnd); 21 | } 22 | 23 | public function test_it_instantiates() 24 | { 25 | $this->assertInstanceOf(DateTimeInterval::class, $this->dateTimeInterval); 26 | } 27 | 28 | public function test_it_creates_a_new_instance() 29 | { 30 | $DeepLinkResources = DateTimeInterval::new(); 31 | 32 | $this->assertInstanceOf(DateTimeInterval::class, $DeepLinkResources); 33 | } 34 | 35 | public function test_it_gets_start() 36 | { 37 | $result = $this->dateTimeInterval->getStart(); 38 | 39 | $this->assertEquals($this->initialStart, $result); 40 | } 41 | 42 | public function test_it_sets_start() 43 | { 44 | $expected = date_create('+1 day'); 45 | 46 | $result = $this->dateTimeInterval->setStart($expected); 47 | 48 | $this->assertSame($this->dateTimeInterval, $result); 49 | $this->assertEquals($expected, $this->dateTimeInterval->getStart()); 50 | } 51 | 52 | public function test_it_gets_end() 53 | { 54 | $result = $this->dateTimeInterval->getEnd(); 55 | 56 | $this->assertEquals($this->initialEnd, $result); 57 | } 58 | 59 | public function test_it_sets_end() 60 | { 61 | $expected = date_create('+1 day'); 62 | 63 | $result = $this->dateTimeInterval->setEnd($expected); 64 | 65 | $this->assertSame($this->dateTimeInterval, $result); 66 | $this->assertEquals($expected, $this->dateTimeInterval->getEnd()); 67 | } 68 | 69 | public function test_it_throws_exception_when_creating_array_with_both_properties_null() 70 | { 71 | $this->dateTimeInterval->setStart(null); 72 | $this->dateTimeInterval->setEnd(null); 73 | 74 | $this->expectException(LtiException::class); 75 | $this->expectExceptionMessage(DateTimeInterval::ERROR_NO_START_OR_END); 76 | 77 | $this->dateTimeInterval->toArray(); 78 | } 79 | 80 | public function test_it_throws_exception_when_creating_array_with_invalid_time_interval() 81 | { 82 | $this->dateTimeInterval->setStart(date_create()); 83 | $this->dateTimeInterval->setEnd(date_create('-1 day')); 84 | 85 | $this->expectException(LtiException::class); 86 | $this->expectExceptionMessage(DateTimeInterval::ERROR_START_GT_END); 87 | 88 | $this->dateTimeInterval->toArray(); 89 | } 90 | 91 | public function test_it_creates_array_with_defined_optional_properties() 92 | { 93 | $expectedStart = date_create('+1 day'); 94 | $expectedEnd = date_create('+2 days'); 95 | $expected = [ 96 | 'startDateTime' => $expectedStart->format(DateTime::ATOM), 97 | 'endDateTime' => $expectedEnd->format(DateTime::ATOM), 98 | ]; 99 | 100 | $this->dateTimeInterval->setStart($expectedStart); 101 | $this->dateTimeInterval->setEnd($expectedEnd); 102 | 103 | $result = $this->dateTimeInterval->toArray(); 104 | 105 | $this->assertEquals($expected, $result); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/DeepLinkResources/IconTest.php: -------------------------------------------------------------------------------- 1 | imageUrl = 'https://example.com/image.png'; 15 | $this->icon = new Icon($this->imageUrl, 1, 2); 16 | } 17 | 18 | public function test_it_instantiates() 19 | { 20 | $this->assertInstanceOf(Icon::class, $this->icon); 21 | } 22 | 23 | public function test_it_creates_a_new_instance() 24 | { 25 | $DeepLinkResources = Icon::new($this->imageUrl, 100, 200); 26 | 27 | $this->assertInstanceOf(Icon::class, $DeepLinkResources); 28 | } 29 | 30 | public function test_it_gets_url() 31 | { 32 | $result = $this->icon->getUrl(); 33 | 34 | $this->assertEquals($this->imageUrl, $result); 35 | } 36 | 37 | public function test_it_sets_url() 38 | { 39 | $expected = 'expected'; 40 | 41 | $this->icon->setUrl($expected); 42 | 43 | $this->assertEquals($expected, $this->icon->getUrl()); 44 | } 45 | 46 | public function test_it_gets_width() 47 | { 48 | $result = $this->icon->getWidth(); 49 | 50 | $this->assertEquals(1, $result); 51 | } 52 | 53 | public function test_it_sets_width() 54 | { 55 | $expected = 300; 56 | 57 | $this->icon->setWidth($expected); 58 | 59 | $this->assertEquals($expected, $this->icon->getWidth()); 60 | } 61 | 62 | public function test_it_gets_height() 63 | { 64 | $result = $this->icon->getHeight(); 65 | 66 | $this->assertEquals(2, $result); 67 | } 68 | 69 | public function test_it_sets_height() 70 | { 71 | $expected = 400; 72 | 73 | $this->icon->setHeight($expected); 74 | 75 | $this->assertEquals($expected, $this->icon->getHeight()); 76 | } 77 | 78 | public function test_it_creates_array() 79 | { 80 | $expected = [ 81 | 'url' => $this->imageUrl, 82 | 'width' => 100, 83 | 'height' => 200, 84 | ]; 85 | 86 | $this->icon->setUrl($expected['url']); 87 | $this->icon->setWidth($expected['width']); 88 | $this->icon->setHeight($expected['height']); 89 | 90 | $result = $this->icon->toArray(); 91 | 92 | $this->assertEquals($expected, $result); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/DeepLinkResources/IframeTest.php: -------------------------------------------------------------------------------- 1 | iframe = new Iframe( 18 | self::INITIAL_SRC, 19 | self::INITIAL_WIDTH, 20 | self::INITIAL_HEIGHT 21 | ); 22 | } 23 | 24 | public function test_it_instantiates() 25 | { 26 | $this->assertInstanceOf(Iframe::class, $this->iframe); 27 | } 28 | 29 | public function test_it_creates_a_new_instance() 30 | { 31 | $DeepLinkResources = Iframe::new(); 32 | 33 | $this->assertInstanceOf(Iframe::class, $DeepLinkResources); 34 | } 35 | 36 | public function test_it_gets_width() 37 | { 38 | $result = $this->iframe->getWidth(); 39 | 40 | $this->assertEquals(self::INITIAL_WIDTH, $result); 41 | } 42 | 43 | public function test_it_sets_width() 44 | { 45 | $expected = 300; 46 | 47 | $result = $this->iframe->setWidth($expected); 48 | 49 | $this->assertSame($this->iframe, $result); 50 | $this->assertEquals($expected, $this->iframe->getWidth()); 51 | } 52 | 53 | public function test_it_gets_height() 54 | { 55 | $result = $this->iframe->getHeight(); 56 | 57 | $this->assertEquals(self::INITIAL_HEIGHT, $result); 58 | } 59 | 60 | public function test_it_sets_height() 61 | { 62 | $expected = 400; 63 | 64 | $result = $this->iframe->setHeight($expected); 65 | 66 | $this->assertSame($this->iframe, $result); 67 | $this->assertEquals($expected, $this->iframe->getHeight()); 68 | } 69 | 70 | public function test_it_gets_src() 71 | { 72 | $result = $this->iframe->getSrc(); 73 | 74 | $this->assertEquals(self::INITIAL_SRC, $result); 75 | } 76 | 77 | public function test_it_sets_src() 78 | { 79 | $expected = 'https://example.com/foo/bar'; 80 | 81 | $result = $this->iframe->setSrc($expected); 82 | 83 | $this->assertSame($this->iframe, $result); 84 | $this->assertEquals($expected, $this->iframe->getSrc()); 85 | } 86 | 87 | public function test_it_creates_array_without_optional_properties() 88 | { 89 | $this->iframe->setWidth(null); 90 | $this->iframe->setHeight(null); 91 | $this->iframe->setSrc(null); 92 | 93 | $result = $this->iframe->toArray(); 94 | 95 | $this->assertEquals([], $result); 96 | } 97 | 98 | public function test_it_creates_array_with_defined_optional_properties() 99 | { 100 | $expected = [ 101 | 'width' => 100, 102 | 'height' => 200, 103 | 'src' => 'https://example.com/foo/bar', 104 | ]; 105 | 106 | $this->iframe->setWidth($expected['width']); 107 | $this->iframe->setHeight($expected['height']); 108 | $this->iframe->setSrc($expected['src']); 109 | 110 | $result = $this->iframe->toArray(); 111 | 112 | $this->assertEquals($expected, $result); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/DeepLinkResources/WindowTest.php: -------------------------------------------------------------------------------- 1 | window = new Window(self::INITIAL_TARGET_NAME, 19 | self::INITIAL_WIDTH, self::INITIAL_HEIGHT, self::INITIAL_WINDOW_FEATURES); 20 | } 21 | 22 | public function test_it_instantiates() 23 | { 24 | $this->assertInstanceOf(Window::class, $this->window); 25 | } 26 | 27 | public function test_it_creates_a_new_instance() 28 | { 29 | $DeepLinkResources = Window::new(); 30 | 31 | $this->assertInstanceOf(Window::class, $DeepLinkResources); 32 | } 33 | 34 | public function test_it_gets_target_name() 35 | { 36 | $result = $this->window->getTargetName(); 37 | 38 | $this->assertEquals(self::INITIAL_TARGET_NAME, $result); 39 | } 40 | 41 | public function test_it_sets_target_name() 42 | { 43 | $expected = 'expected'; 44 | 45 | $result = $this->window->setTargetName($expected); 46 | 47 | $this->assertSame($this->window, $result); 48 | $this->assertEquals($expected, $this->window->getTargetName()); 49 | } 50 | 51 | public function test_it_gets_width() 52 | { 53 | $result = $this->window->getWidth(); 54 | 55 | $this->assertEquals(self::INITIAL_WIDTH, $result); 56 | } 57 | 58 | public function test_it_sets_width() 59 | { 60 | $expected = 300; 61 | 62 | $result = $this->window->setWidth($expected); 63 | 64 | $this->assertSame($this->window, $result); 65 | $this->assertEquals($expected, $this->window->getWidth()); 66 | } 67 | 68 | public function test_it_gets_height() 69 | { 70 | $result = $this->window->getHeight(); 71 | 72 | $this->assertEquals(self::INITIAL_HEIGHT, $result); 73 | } 74 | 75 | public function test_it_sets_height() 76 | { 77 | $expected = 400; 78 | 79 | $result = $this->window->setHeight($expected); 80 | 81 | $this->assertSame($this->window, $result); 82 | $this->assertEquals($expected, $this->window->getHeight()); 83 | } 84 | 85 | public function test_it_gets_window_features() 86 | { 87 | $result = $this->window->getWindowFeatures(); 88 | 89 | $this->assertEquals(self::INITIAL_WINDOW_FEATURES, $result); 90 | } 91 | 92 | public function test_it_sets_window_features() 93 | { 94 | $expected = 'first-feature=value,second-feature'; 95 | 96 | $result = $this->window->setWindowFeatures($expected); 97 | 98 | $this->assertSame($this->window, $result); 99 | $this->assertEquals($expected, $this->window->getWindowFeatures()); 100 | } 101 | 102 | public function test_it_creates_array() 103 | { 104 | $expected = [ 105 | 'targetName' => 'target-name', 106 | 'width' => 100, 107 | 'height' => 200, 108 | 'windowFeatures' => 'first-feature=value,second-feature', 109 | ]; 110 | 111 | $this->window->setTargetName($expected['targetName']); 112 | $this->window->setWidth($expected['width']); 113 | $this->window->setHeight($expected['height']); 114 | $this->window->setWindowFeatures($expected['windowFeatures']); 115 | 116 | $result = $this->window->toArray(); 117 | 118 | $this->assertEquals($expected, $result); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/Helpers/HelpersTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $actual); 16 | } 17 | 18 | public function test_it_builds_a_url_with_params() 19 | { 20 | $baseUrl = 'https://www.example.com'; 21 | $actual = Helpers::buildUrlWithQueryParams($baseUrl, ['foo' => 'bar']); 22 | 23 | $this->assertEquals('https://www.example.com?foo=bar', $actual); 24 | } 25 | 26 | public function test_it_builds_a_url_with_existing_params() 27 | { 28 | $baseUrl = 'https://www.example.com?baz=bat'; 29 | $actual = Helpers::buildUrlWithQueryParams($baseUrl, ['foo' => 'bar']); 30 | 31 | $this->assertEquals('https://www.example.com?baz=bat&foo=bar', $actual); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/JwksEndpointTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(JwksEndpoint::class, $jwks); 17 | } 18 | 19 | public function test_creates_a_new_instance() 20 | { 21 | $jwks = JwksEndpoint::new([]); 22 | 23 | $this->assertInstanceOf(JwksEndpoint::class, $jwks); 24 | } 25 | 26 | public function test_creates_a_new_instance_from_issuer() 27 | { 28 | $database = Mockery::mock(IDatabase::class); 29 | $registration = Mockery::mock(ILtiRegistration::class); 30 | $database->shouldReceive('findRegistrationByIssuer') 31 | ->once() 32 | ->andReturn($registration); 33 | $registration->shouldReceive('getKid') 34 | ->once() 35 | ->andReturn('kid'); 36 | $registration->shouldReceive('getToolPrivateKey') 37 | ->once() 38 | ->andReturn('private_key'); 39 | 40 | $jwks = JwksEndpoint::fromIssuer($database, 'issuer'); 41 | 42 | $this->assertInstanceOf(JwksEndpoint::class, $jwks); 43 | } 44 | 45 | public function test_creates_a_new_instance_from_registration() 46 | { 47 | $registration = Mockery::mock(ILtiRegistration::class); 48 | $registration->shouldReceive('getKid') 49 | ->once() 50 | ->andReturn('kid'); 51 | $registration->shouldReceive('getToolPrivateKey') 52 | ->once() 53 | ->andReturn('private_key'); 54 | 55 | $jwks = JwksEndpoint::fromRegistration($registration); 56 | 57 | $this->assertInstanceOf(JwksEndpoint::class, $jwks); 58 | } 59 | 60 | public function test_it_gets_jwks_for_the_provided_keys() 61 | { 62 | $jwks = new JwksEndpoint([ 63 | 'kid' => file_get_contents(__DIR__.'/data/private.key'), 64 | ]); 65 | 66 | $result = $jwks->getPublicJwks(); 67 | 68 | $this->assertEquals(['keys' => [[ 69 | 'kty' => 'RSA', 70 | 'alg' => 'RS256', 71 | 'use' => 'sig', 72 | 'e' => 'AQAB', 73 | 'n' => '6DzRJzrx0KThi0piO3wdNA3e7-xXly5WJo00CqlKDodtyX6wRT76E4cD57yrr_ZWuaA-6idSFPaEQXw9tCqqTIrS4STIYrlvC0CeEA7m0s2PbI2ffaxv2kofxdmOaUI8YW8NIqNyHMl6Acz1lQOOZ5xSreG5JAqtZpy7AwDdpJo7up9937AD9ZV77qlty6xRKVqOGP1-cH97zMvlQo0EUWUhRAzDlTlCXnbeSjVypET3l93WPT9gnIywt1xX0L6rIJd-4fyU6faaToGN9z4_Q6ay2xFSEJnoNBW9wI886W75vLcVLnT95YKJJwZoKEa9yoV_ZPiTBJcFv1HFPf4ibQ', 74 | 'kid' => 'kid', 75 | ]]], $result); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Lti1p1KeyTest.php: -------------------------------------------------------------------------------- 1 | key = new Lti1p1Key; 13 | } 14 | 15 | public function test_it_instantiates() 16 | { 17 | $this->assertInstanceOf(Lti1p1Key::class, $this->key); 18 | } 19 | 20 | public function test_it_gets_key() 21 | { 22 | $result = $this->key->getKey(); 23 | 24 | $this->assertNull($result); 25 | } 26 | 27 | public function test_it_sets_key() 28 | { 29 | $expected = 'expected'; 30 | 31 | $this->key->setKey($expected); 32 | 33 | $this->assertEquals($expected, $this->key->getKey()); 34 | } 35 | 36 | public function test_it_gets_secret() 37 | { 38 | $result = $this->key->getSecret(); 39 | 40 | $this->assertNull($result); 41 | } 42 | 43 | public function test_it_sets_secret() 44 | { 45 | $expected = 'expected'; 46 | 47 | $this->key->setSecret($expected); 48 | 49 | $this->assertEquals($expected, $this->key->getSecret()); 50 | } 51 | 52 | public function test_it_signs() 53 | { 54 | $key = new Lti1p1Key([ 55 | 'key' => 'foo', 56 | 'secret' => 'bar', 57 | ]); 58 | 59 | $actual = $key->sign('deploymentId', 'iss', 'clientId', 'exp', 'nonce'); 60 | 61 | $this->assertEquals('1Ze6akG0koOVeizCVBIyQHJ78Eo3vGUXyqOM0iDqS0k=', $actual); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/LtiCourseGroupsServiceTest.php: -------------------------------------------------------------------------------- 1 | connector = Mockery::mock(ILtiServiceConnector::class); 17 | $this->registration = Mockery::mock(ILtiRegistration::class); 18 | } 19 | 20 | public function test_it_instantiates() 21 | { 22 | $service = new LtiCourseGroupsService($this->connector, $this->registration, []); 23 | 24 | $this->assertInstanceOf(LtiCourseGroupsService::class, $service); 25 | } 26 | 27 | public function test_it_gets_scope() 28 | { 29 | $serviceData = [ 30 | 'scope' => ['asdf'], 31 | ]; 32 | 33 | $service = new LtiCourseGroupsService($this->connector, $this->registration, $serviceData); 34 | 35 | $result = $service->getScope(); 36 | 37 | $this->assertEquals($serviceData['scope'], $result); 38 | } 39 | 40 | public function test_it_gets_groups() 41 | { 42 | $serviceData = [ 43 | 'context_groups_url' => 'https://example.com', 44 | 'scope' => ['asdf'], 45 | ]; 46 | 47 | $service = new LtiCourseGroupsService($this->connector, $this->registration, $serviceData); 48 | 49 | $expected = [ 50 | 'id' => 'testId', 51 | ]; 52 | 53 | $this->connector->shouldReceive('getAll') 54 | ->once()->andReturn($expected); 55 | 56 | $result = $service->getGroups(); 57 | 58 | $this->assertEquals($expected, $result); 59 | } 60 | 61 | public function test_it_gets_sets() 62 | { 63 | $serviceData = [ 64 | 'context_group_sets_url' => 'https://example.com', 65 | 'scope' => ['asdf'], 66 | ]; 67 | 68 | $service = new LtiCourseGroupsService($this->connector, $this->registration, $serviceData); 69 | 70 | $expected = [ 71 | 'id' => 'testId', 72 | ]; 73 | 74 | $this->connector->shouldReceive('getAll') 75 | ->once()->andReturn($expected); 76 | 77 | $result = $service->getSets(); 78 | 79 | $this->assertEquals($expected, $result); 80 | } 81 | 82 | public function test_it_get_groups_by_set() 83 | { 84 | $serviceData = [ 85 | 'context_groups_url' => 'https://example.com', 86 | 'context_group_sets_url' => 'https://example.com', 87 | 'scope' => ['asdf'], 88 | ]; 89 | 90 | $service = new LtiCourseGroupsService($this->connector, $this->registration, $serviceData); 91 | 92 | $groups = [ 93 | [ 94 | 'id' => 'testId', 95 | 'set_id' => 'testSetId', 96 | ], 97 | [ 98 | 'id' => 'testId2', 99 | 'set_id' => 'testSetId', 100 | ], 101 | [ 102 | 'id' => 'testId3', 103 | 'set_id' => 'testSetId2', 104 | ], 105 | [ 106 | 'id' => 'noSetId', 107 | ], 108 | ]; 109 | $sets = [ 110 | [ 111 | 'id' => 'testSetId', 112 | ], 113 | [ 114 | 'id' => 'testSetId2', 115 | ], 116 | ]; 117 | 118 | $expected = [ 119 | 'testSetId' => [ 120 | 'id' => 'testSetId', 121 | 'groups' => [ 122 | 'testId' => [ 123 | 'id' => 'testId', 124 | 'set_id' => 'testSetId', 125 | ], 126 | 'testId2' => [ 127 | 'id' => 'testId2', 128 | 'set_id' => 'testSetId', 129 | ], 130 | ], 131 | ], 132 | 'testSetId2' => [ 133 | 'id' => 'testSetId2', 134 | 'groups' => [ 135 | 'testId3' => [ 136 | 'id' => 'testId3', 137 | 'set_id' => 'testSetId2', 138 | ], 139 | ], 140 | ], 141 | 'none' => [ 142 | 'name' => 'None', 143 | 'id' => 'none', 144 | 'groups' => [ 145 | 'noSetId' => [ 146 | 'id' => 'noSetId', 147 | ], 148 | ], 149 | ], 150 | ]; 151 | 152 | $this->connector->shouldReceive('getAll') 153 | ->once()->andReturn($groups); 154 | $this->connector->shouldReceive('getAll') 155 | ->once()->andReturn($sets); 156 | 157 | $result = $service->getGroupsBySet(); 158 | 159 | $this->assertEquals($expected, $result); 160 | } 161 | 162 | public function test_it_gets_no_sets_for_no_url() 163 | { 164 | $service = new LtiCourseGroupsService($this->connector, $this->registration, []); 165 | 166 | $result = $service->getSets(); 167 | 168 | $this->assertEquals([], $result); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/LtiDeploymentTest.php: -------------------------------------------------------------------------------- 1 | deployment = new LtiDeployment($this->id); 14 | } 15 | 16 | public function test_it_instantiates() 17 | { 18 | $this->assertInstanceOf(LtiDeployment::class, $this->deployment); 19 | } 20 | 21 | public function test_creates_a_new_instance() 22 | { 23 | $deployment = LtiDeployment::new($this->id); 24 | 25 | $this->assertInstanceOf(LtiDeployment::class, $deployment); 26 | } 27 | 28 | public function test_it_gets_deployment_id() 29 | { 30 | $result = $this->deployment->getDeploymentId(); 31 | 32 | $this->assertEquals($this->id, $result); 33 | } 34 | 35 | public function test_it_sets_deployment_id() 36 | { 37 | $expected = 'expected'; 38 | 39 | $this->deployment->setDeploymentId($expected); 40 | 41 | $this->assertEquals($expected, $this->deployment->getDeploymentId()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/LtiGradeSubmissionReviewTest.php: -------------------------------------------------------------------------------- 1 | gradeReview = new LtiGradeSubmissionReview; 14 | } 15 | 16 | public function test_it_instantiates() 17 | { 18 | $this->assertInstanceOf(LtiGradeSubmissionReview::class, $this->gradeReview); 19 | } 20 | 21 | public function test_creates_a_new_instance() 22 | { 23 | $review = LtiGradeSubmissionReview::new(); 24 | 25 | $this->assertInstanceOf(LtiGradeSubmissionReview::class, $review); 26 | } 27 | 28 | public function test_it_gets_reviewable_status() 29 | { 30 | $expected = 'ReviewableStatus'; 31 | $gradeReview = new LtiGradeSubmissionReview(['reviewableStatus' => 'ReviewableStatus']); 32 | 33 | $result = $gradeReview->getReviewableStatus(); 34 | 35 | $this->assertEquals($expected, $result); 36 | } 37 | 38 | public function test_it_sets_reviewable_status() 39 | { 40 | $expected = 'expected'; 41 | 42 | $this->gradeReview->setReviewableStatus($expected); 43 | 44 | $this->assertEquals($expected, $this->gradeReview->getReviewableStatus()); 45 | } 46 | 47 | public function test_it_gets_label() 48 | { 49 | $expected = 'Label'; 50 | $gradeReview = new LtiGradeSubmissionReview(['label' => 'Label']); 51 | 52 | $result = $gradeReview->getLabel(); 53 | 54 | $this->assertEquals($expected, $result); 55 | } 56 | 57 | public function test_it_sets_label() 58 | { 59 | $expected = 'expected'; 60 | 61 | $this->gradeReview->setLabel($expected); 62 | 63 | $this->assertEquals($expected, $this->gradeReview->getLabel()); 64 | } 65 | 66 | public function test_it_gets_url() 67 | { 68 | $expected = 'Url'; 69 | $gradeReview = new LtiGradeSubmissionReview(['url' => 'Url']); 70 | 71 | $result = $gradeReview->getUrl(); 72 | 73 | $this->assertEquals($expected, $result); 74 | } 75 | 76 | public function test_it_sets_url() 77 | { 78 | $expected = 'expected'; 79 | 80 | $this->gradeReview->setUrl($expected); 81 | 82 | $this->assertEquals($expected, $this->gradeReview->getUrl()); 83 | } 84 | 85 | public function test_it_gets_custom() 86 | { 87 | $expected = 'Custom'; 88 | $gradeReview = new LtiGradeSubmissionReview(['custom' => 'Custom']); 89 | 90 | $result = $gradeReview->getCustom(); 91 | 92 | $this->assertEquals($expected, $result); 93 | } 94 | 95 | public function test_it_sets_custom() 96 | { 97 | $expected = 'expected'; 98 | 99 | $this->gradeReview->setCustom($expected); 100 | 101 | $this->assertEquals($expected, $this->gradeReview->getCustom()); 102 | } 103 | 104 | public function test_it_casts_full_object_to_string() 105 | { 106 | $expected = [ 107 | 'reviewableStatus' => 'ReviewableStatus', 108 | 'label' => 'Label', 109 | 'url' => 'Url', 110 | 'custom' => 'Custom', 111 | ]; 112 | 113 | $gradeReview = new LtiGradeSubmissionReview($expected); 114 | 115 | $this->assertEquals(json_encode($expected), (string) $gradeReview); 116 | } 117 | 118 | public function test_it_casts_empty_object_to_string() 119 | { 120 | $this->assertEquals('[]', (string) $this->gradeReview); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/LtiLineitemTest.php: -------------------------------------------------------------------------------- 1 | lineItem = new LtiLineitem; 14 | } 15 | 16 | public function test_it_instantiates() 17 | { 18 | $this->assertInstanceOf(LtiLineitem::class, $this->lineItem); 19 | } 20 | 21 | public function test_it_creates_a_new_instance() 22 | { 23 | $grade = LtiLineitem::new(); 24 | 25 | $this->assertInstanceOf(LtiLineitem::class, $grade); 26 | } 27 | 28 | public function test_it_gets_id() 29 | { 30 | $expected = 'expected'; 31 | $grade = new LtiLineitem(['id' => $expected]); 32 | 33 | $result = $grade->getId(); 34 | 35 | $this->assertEquals($expected, $result); 36 | } 37 | 38 | public function test_it_sets_id() 39 | { 40 | $expected = 'expected'; 41 | 42 | $this->lineItem->setId($expected); 43 | 44 | $this->assertEquals($expected, $this->lineItem->getId()); 45 | } 46 | 47 | public function test_it_gets_score_maximum() 48 | { 49 | $expected = 'expected'; 50 | $grade = new LtiLineitem(['scoreMaximum' => $expected]); 51 | 52 | $result = $grade->getScoreMaximum(); 53 | 54 | $this->assertEquals($expected, $result); 55 | } 56 | 57 | public function test_it_sets_score_maximum() 58 | { 59 | $expected = 'expected'; 60 | 61 | $this->lineItem->setScoreMaximum($expected); 62 | 63 | $this->assertEquals($expected, $this->lineItem->getScoreMaximum()); 64 | } 65 | 66 | public function test_it_gets_label() 67 | { 68 | $expected = 'expected'; 69 | $grade = new LtiLineitem(['label' => $expected]); 70 | 71 | $result = $grade->getLabel(); 72 | 73 | $this->assertEquals($expected, $result); 74 | } 75 | 76 | public function test_it_sets_label() 77 | { 78 | $expected = 'expected'; 79 | 80 | $this->lineItem->setLabel($expected); 81 | 82 | $this->assertEquals($expected, $this->lineItem->getLabel()); 83 | } 84 | 85 | public function test_it_gets_resource_id() 86 | { 87 | $expected = 'expected'; 88 | $grade = new LtiLineitem(['resourceId' => $expected]); 89 | 90 | $result = $grade->getResourceId(); 91 | 92 | $this->assertEquals($expected, $result); 93 | } 94 | 95 | public function test_it_sets_resource_id() 96 | { 97 | $expected = 'expected'; 98 | 99 | $this->lineItem->setResourceId($expected); 100 | 101 | $this->assertEquals($expected, $this->lineItem->getResourceId()); 102 | } 103 | 104 | public function test_it_gets_resource_link_id() 105 | { 106 | $expected = 'expected'; 107 | $grade = new LtiLineitem(['resourceLinkId' => $expected]); 108 | 109 | $result = $grade->getResourceLinkId(); 110 | 111 | $this->assertEquals($expected, $result); 112 | } 113 | 114 | public function test_it_sets_resource_link_id() 115 | { 116 | $expected = 'expected'; 117 | 118 | $this->lineItem->setResourceLinkId($expected); 119 | 120 | $this->assertEquals($expected, $this->lineItem->getResourceLinkId()); 121 | } 122 | 123 | public function test_it_gets_tag() 124 | { 125 | $expected = 'expected'; 126 | $grade = new LtiLineitem(['tag' => $expected]); 127 | 128 | $result = $grade->getTag(); 129 | 130 | $this->assertEquals($expected, $result); 131 | } 132 | 133 | public function test_it_sets_tag() 134 | { 135 | $expected = 'expected'; 136 | 137 | $this->lineItem->setTag($expected); 138 | 139 | $this->assertEquals($expected, $this->lineItem->getTag()); 140 | } 141 | 142 | public function test_it_gets_start_date_time() 143 | { 144 | $expected = 'expected'; 145 | $grade = new LtiLineitem(['startDateTime' => $expected]); 146 | 147 | $result = $grade->getStartDateTime(); 148 | 149 | $this->assertEquals($expected, $result); 150 | } 151 | 152 | public function test_it_sets_start_date_time() 153 | { 154 | $expected = 'expected'; 155 | 156 | $this->lineItem->setStartDateTime($expected); 157 | 158 | $this->assertEquals($expected, $this->lineItem->getStartDateTime()); 159 | } 160 | 161 | public function test_it_gets_end_date_time() 162 | { 163 | $expected = 'expected'; 164 | $grade = new LtiLineitem(['endDateTime' => $expected]); 165 | 166 | $result = $grade->getEndDateTime(); 167 | 168 | $this->assertEquals($expected, $result); 169 | } 170 | 171 | public function test_it_sets_end_date_time() 172 | { 173 | $expected = 'expected'; 174 | 175 | $this->lineItem->setEndDateTime($expected); 176 | 177 | $this->assertEquals($expected, $this->lineItem->getEndDateTime()); 178 | } 179 | 180 | public function test_it_gets_grades_released(): void 181 | { 182 | $expected = true; 183 | $grade = new LtiLineitem(['gradesReleased' => $expected]); 184 | 185 | $result = $grade->getGradesReleased(); 186 | 187 | $this->assertEquals($expected, $result); 188 | } 189 | 190 | public function test_it_sets_grades_released(): void 191 | { 192 | $expected = false; 193 | 194 | $this->lineItem->setGradesReleased($expected); 195 | 196 | $this->assertEquals($expected, $this->lineItem->getGradesReleased()); 197 | } 198 | 199 | public function test_grades_released_constructed_nullable(): void 200 | { 201 | $grade = new LtiLineitem; 202 | 203 | $result = $grade->getGradesReleased(); 204 | 205 | $this->assertNull($result); 206 | } 207 | 208 | public function test_grades_released_set_nullable(): void 209 | { 210 | $this->lineItem->setGradesReleased(null); 211 | 212 | $this->assertNull($this->lineItem->getGradesReleased()); 213 | } 214 | 215 | public function test_it_casts_full_object_to_string() 216 | { 217 | $expected = [ 218 | 'id' => 'Id', 219 | 'scoreMaximum' => 'ScoreMaximum', 220 | 'label' => 'Label', 221 | 'resourceId' => 'ResourceId', 222 | 'resourceLinkId' => 'ResourceLinkId', 223 | 'tag' => 'Tag', 224 | 'startDateTime' => 'StartDateTime', 225 | 'endDateTime' => 'EndDateTime', 226 | 'gradesReleased' => true, 227 | ]; 228 | 229 | $lineItem = new LtiLineitem($expected); 230 | 231 | $this->assertEquals(json_encode($expected), (string) $lineItem); 232 | } 233 | 234 | public function test_it_casts_empty_object_to_string() 235 | { 236 | $this->assertEquals('[]', (string) $this->lineItem); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /tests/LtiNamesRolesProvisioningServiceTest.php: -------------------------------------------------------------------------------- 1 | connector = Mockery::mock(ILtiServiceConnector::class); 17 | $this->registration = Mockery::mock(ILtiRegistration::class); 18 | } 19 | 20 | public function test_it_instantiates() 21 | { 22 | $nrps = new LtiNamesRolesProvisioningService($this->connector, $this->registration, []); 23 | 24 | $this->assertInstanceOf(LtiNamesRolesProvisioningService::class, $nrps); 25 | } 26 | 27 | public function test_it_gets_members() 28 | { 29 | $expected = ['members']; 30 | 31 | $nrps = new LtiNamesRolesProvisioningService($this->connector, $this->registration, [ 32 | 'context_memberships_url' => 'url', 33 | ]); 34 | $this->connector->shouldReceive('getAll') 35 | ->once()->andReturn($expected); 36 | 37 | $result = $nrps->getMembers(); 38 | 39 | $this->assertEquals($expected, $result); 40 | } 41 | 42 | public function test_it_gets_members_for_resource_link() 43 | { 44 | $expected = ['members']; 45 | 46 | $nrps = new LtiNamesRolesProvisioningService($this->connector, $this->registration, [ 47 | 'context_memberships_url' => 'url', 48 | ]); 49 | $this->connector->shouldReceive('getAll') 50 | ->withArgs(function ($registration, $scope, $request, $key) { 51 | return $request->getUrl() === 'url?rlid=resource-link-id' && $key === 'members'; 52 | }) 53 | ->once()->andReturn($expected); 54 | 55 | $result = $nrps->getMembers(['rlid' => 'resource-link-id']); 56 | 57 | $this->assertEquals($expected, $result); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/LtiOidcLoginTest.php: -------------------------------------------------------------------------------- 1 | cache = Mockery::mock(ICache::class); 23 | $this->cookie = Mockery::mock(ICookie::class); 24 | $this->database = Mockery::mock(IDatabase::class); 25 | 26 | $this->oidcLogin = new LtiOidcLogin( 27 | $this->database, 28 | $this->cache, 29 | $this->cookie 30 | ); 31 | } 32 | 33 | public function test_it_instantiates() 34 | { 35 | $this->assertInstanceOf(LtiOidcLogin::class, $this->oidcLogin); 36 | } 37 | 38 | public function test_it_creates_a_new_instance() 39 | { 40 | $oidcLogin = LtiOidcLogin::new( 41 | $this->database, 42 | $this->cache, 43 | $this->cookie 44 | ); 45 | 46 | $this->assertInstanceOf(LtiOidcLogin::class, $this->oidcLogin); 47 | } 48 | 49 | public function test_it_validates_a_request() 50 | { 51 | $expected = Mockery::mock(ILtiRegistration::class); 52 | $request = [ 53 | 'iss' => 'Issuer', 54 | 'login_hint' => 'LoginHint', 55 | 'client_id' => 'ClientId', 56 | ]; 57 | 58 | $this->database->shouldReceive('findRegistrationByIssuer') 59 | ->once()->with($request['iss'], $request['client_id']) 60 | ->andReturn($expected); 61 | 62 | $result = $this->oidcLogin->validateOidcLogin($request); 63 | 64 | $this->assertEquals($expected, $result); 65 | } 66 | 67 | public function test_validates_fails_if_issuer_is_not_set() 68 | { 69 | $request = [ 70 | 'login_hint' => 'LoginHint', 71 | 'client_id' => 'ClientId', 72 | ]; 73 | 74 | $this->expectException(OidcException::class); 75 | $this->expectExceptionMessage(LtiOidcLogin::ERROR_MSG_ISSUER); 76 | 77 | $this->oidcLogin->validateOidcLogin($request); 78 | } 79 | 80 | public function test_validates_fails_if_login_hint_is_not_set() 81 | { 82 | $request = [ 83 | 'iss' => 'Issuer', 84 | 'client_id' => 'ClientId', 85 | ]; 86 | 87 | $this->expectException(OidcException::class); 88 | $this->expectExceptionMessage(LtiOidcLogin::ERROR_MSG_LOGIN_HINT); 89 | 90 | $this->oidcLogin->validateOidcLogin($request); 91 | } 92 | 93 | /** 94 | * @runInSeparateProcess 95 | * 96 | * @preserveGlobalState disabled 97 | */ 98 | public function test_validates_fails_if_registration_not_found() 99 | { 100 | $request = [ 101 | 'iss' => 'Issuer', 102 | 'login_hint' => 'LoginHint', 103 | ]; 104 | $this->database->shouldReceive('findRegistrationByIssuer') 105 | ->once()->andReturn(null); 106 | 107 | // Use an alias to mock LtiMessageLaunch::getMissingRegistrationErrorMsg() 108 | $expectedError = 'Registration not found!'; 109 | Mockery::mock('alias:'.LtiMessageLaunch::class) 110 | ->shouldReceive('getMissingRegistrationErrorMsg') 111 | ->andReturn($expectedError); 112 | 113 | $this->expectException(OidcException::class); 114 | $this->expectExceptionMessage($expectedError); 115 | 116 | $this->oidcLogin->validateOidcLogin($request); 117 | } 118 | 119 | public function test_get_auth_params() 120 | { 121 | $this->cookie->shouldReceive('setCookie') 122 | ->once(); 123 | $this->cache->shouldReceive('cacheNonce') 124 | ->once(); 125 | 126 | $launchUrl = 'https://example.com/launch'; 127 | $clientId = 'ClientId'; 128 | $expected = [ 129 | 'scope' => 'openid', 130 | 'response_type' => 'id_token', 131 | 'response_mode' => 'form_post', 132 | 'prompt' => 'none', 133 | 'client_id' => $clientId, 134 | 'redirect_uri' => $launchUrl, 135 | 'login_hint' => 'LoginHint', 136 | 'lti_message_hint' => 'LtiMessageHint', 137 | ]; 138 | $request = [ 139 | 'login_hint' => 'LoginHint', 140 | 'lti_message_hint' => 'LtiMessageHint', 141 | ]; 142 | 143 | $result = $this->oidcLogin->getAuthParams($launchUrl, $clientId, $request); 144 | 145 | // These are cryptographically random, so just assert they exist 146 | $this->assertArrayHasKey('state', $result); 147 | $this->assertArrayHasKey('nonce', $result); 148 | // No remove them and check equality 149 | unset($result['state'], $result['nonce']); 150 | $this->assertEquals($expected, $result); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/LtiRegistrationTest.php: -------------------------------------------------------------------------------- 1 | registration = new LtiRegistration; 14 | } 15 | 16 | public function test_it_instantiates() 17 | { 18 | $this->assertInstanceOf(LtiRegistration::class, $this->registration); 19 | } 20 | 21 | public function test_it_creates_a_new_instance() 22 | { 23 | $registration = LtiRegistration::new(); 24 | 25 | $this->assertInstanceOf(LtiRegistration::class, $registration); 26 | } 27 | 28 | public function test_it_gets_issuer() 29 | { 30 | $expected = 'expected'; 31 | $registration = new LtiRegistration(['issuer' => $expected]); 32 | 33 | $result = $registration->getIssuer(); 34 | 35 | $this->assertEquals($expected, $result); 36 | } 37 | 38 | public function test_it_sets_issuer() 39 | { 40 | $expected = 'expected'; 41 | 42 | $this->registration->setIssuer($expected); 43 | 44 | $this->assertEquals($expected, $this->registration->getIssuer()); 45 | } 46 | 47 | public function test_it_gets_client_id() 48 | { 49 | $expected = 'expected'; 50 | $registration = new LtiRegistration(['clientId' => $expected]); 51 | 52 | $result = $registration->getClientId(); 53 | 54 | $this->assertEquals($expected, $result); 55 | } 56 | 57 | public function test_it_sets_client_id() 58 | { 59 | $expected = 'expected'; 60 | 61 | $this->registration->setClientId($expected); 62 | 63 | $this->assertEquals($expected, $this->registration->getClientId()); 64 | } 65 | 66 | public function test_it_gets_key_set_url() 67 | { 68 | $expected = 'expected'; 69 | $registration = new LtiRegistration(['keySetUrl' => $expected]); 70 | 71 | $result = $registration->getKeySetUrl(); 72 | 73 | $this->assertEquals($expected, $result); 74 | } 75 | 76 | public function test_it_sets_key_set_url() 77 | { 78 | $expected = 'expected'; 79 | 80 | $this->registration->setKeySetUrl($expected); 81 | 82 | $this->assertEquals($expected, $this->registration->getKeySetUrl()); 83 | } 84 | 85 | public function test_it_gets_auth_token_url() 86 | { 87 | $expected = 'expected'; 88 | $registration = new LtiRegistration(['authTokenUrl' => $expected]); 89 | 90 | $result = $registration->getAuthTokenUrl(); 91 | 92 | $this->assertEquals($expected, $result); 93 | } 94 | 95 | public function test_it_sets_auth_token_url() 96 | { 97 | $expected = 'expected'; 98 | 99 | $this->registration->setAuthTokenUrl($expected); 100 | 101 | $this->assertEquals($expected, $this->registration->getAuthTokenUrl()); 102 | } 103 | 104 | public function test_it_gets_auth_login_url() 105 | { 106 | $expected = 'expected'; 107 | $registration = new LtiRegistration(['authLoginUrl' => $expected]); 108 | 109 | $result = $registration->getAuthLoginUrl(); 110 | 111 | $this->assertEquals($expected, $result); 112 | } 113 | 114 | public function test_it_sets_auth_login_url() 115 | { 116 | $expected = 'expected'; 117 | 118 | $this->registration->setAuthLoginUrl($expected); 119 | 120 | $this->assertEquals($expected, $this->registration->getAuthLoginUrl()); 121 | } 122 | 123 | public function test_it_gets_auth_server() 124 | { 125 | $expected = 'expected'; 126 | $registration = new LtiRegistration(['authServer' => $expected]); 127 | 128 | $result = $registration->getAuthServer(); 129 | 130 | $this->assertEquals($expected, $result); 131 | } 132 | 133 | public function test_it_sets_auth_server() 134 | { 135 | $expected = 'expected'; 136 | 137 | $this->registration->setAuthServer($expected); 138 | 139 | $this->assertEquals($expected, $this->registration->getAuthServer()); 140 | } 141 | 142 | public function test_it_gets_tool_private_key() 143 | { 144 | $expected = 'expected'; 145 | $registration = new LtiRegistration(['toolPrivateKey' => $expected]); 146 | 147 | $result = $registration->getToolPrivateKey(); 148 | 149 | $this->assertEquals($expected, $result); 150 | } 151 | 152 | public function test_it_sets_tool_private_key() 153 | { 154 | $expected = 'expected'; 155 | 156 | $this->registration->setToolPrivateKey($expected); 157 | 158 | $this->assertEquals($expected, $this->registration->getToolPrivateKey()); 159 | } 160 | 161 | public function test_it_gets_kid() 162 | { 163 | $expected = 'expected'; 164 | $registration = new LtiRegistration(['kid' => $expected]); 165 | 166 | $result = $registration->getKid(); 167 | 168 | $this->assertEquals($expected, $result); 169 | } 170 | 171 | public function test_it_gets_kid_from_issuer_and_client_id() 172 | { 173 | $expected = '39e02c46a08382b7b352b4f1a9d38698b8fe7c8eb74ead609c804b25eeb1db52'; 174 | $registration = new LtiRegistration([ 175 | 'issuer' => 'Issuer', 176 | 'client_id' => 'ClientId', 177 | ]); 178 | 179 | $result = $registration->getKid(); 180 | 181 | $this->assertEquals($expected, $result); 182 | } 183 | 184 | public function test_it_sets_kid() 185 | { 186 | $expected = 'expected'; 187 | 188 | $this->registration->setKid($expected); 189 | 190 | $this->assertEquals($expected, $this->registration->getKid()); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /tests/MessageValidators/DeepLinkMessageValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(DeepLinkMessageValidator::canValidate(self::validJwtBody())); 15 | } 16 | 17 | public function test_it_cannot_validate() 18 | { 19 | $jwtBody = self::validJwtBody(); 20 | $jwtBody[LtiConstants::MESSAGE_TYPE] = 'some other type'; 21 | 22 | $this->assertFalse(DeepLinkMessageValidator::canValidate($jwtBody)); 23 | } 24 | 25 | public function test_jwt_body_is_valid() 26 | { 27 | $this->assertNull(DeepLinkMessageValidator::validate(self::validJwtBody())); 28 | } 29 | 30 | public function test_jwt_body_is_invalid_missing_sub() 31 | { 32 | $jwtBody = self::validJwtBody(); 33 | $jwtBody['sub'] = ''; 34 | 35 | $this->expectException(LtiException::class); 36 | 37 | DeepLinkMessageValidator::validate($jwtBody); 38 | } 39 | 40 | public function test_jwt_body_is_invalid_missing_lti_version() 41 | { 42 | $jwtBody = self::validJwtBody(); 43 | unset($jwtBody[LtiConstants::VERSION]); 44 | 45 | $this->expectException(LtiException::class); 46 | 47 | DeepLinkMessageValidator::validate($jwtBody); 48 | } 49 | 50 | public function test_jwt_body_is_invalid_wrong_lti_version() 51 | { 52 | $jwtBody = self::validJwtBody(); 53 | $jwtBody[LtiConstants::VERSION] = '1.2.0'; 54 | 55 | $this->expectException(LtiException::class); 56 | 57 | DeepLinkMessageValidator::validate($jwtBody); 58 | } 59 | 60 | public function test_jwt_body_is_invalid_missing_roles() 61 | { 62 | $jwtBody = self::validJwtBody(); 63 | unset($jwtBody[LtiConstants::ROLES]); 64 | 65 | $this->expectException(LtiException::class); 66 | 67 | DeepLinkMessageValidator::validate($jwtBody); 68 | } 69 | 70 | public function test_jwt_body_is_invalid_missing_deep_link_setting() 71 | { 72 | $jwtBody = self::validJwtBody(); 73 | unset($jwtBody[LtiConstants::DL_DEEP_LINK_SETTINGS]); 74 | 75 | $this->expectException(LtiException::class); 76 | 77 | DeepLinkMessageValidator::validate($jwtBody); 78 | } 79 | 80 | public function test_jwt_body_is_invalid_missing_deep_link_return_url() 81 | { 82 | $jwtBody = self::validJwtBody(); 83 | unset($jwtBody[LtiConstants::DL_DEEP_LINK_SETTINGS]['deep_link_return_url']); 84 | 85 | $this->expectException(LtiException::class); 86 | 87 | DeepLinkMessageValidator::validate($jwtBody); 88 | } 89 | 90 | public function test_jwt_body_is_invalid_missing_accept_type() 91 | { 92 | $jwtBody = self::validJwtBody(); 93 | unset($jwtBody[LtiConstants::DL_DEEP_LINK_SETTINGS]['accept_types']); 94 | 95 | $this->expectException(LtiException::class); 96 | 97 | DeepLinkMessageValidator::validate($jwtBody); 98 | } 99 | 100 | public function test_jwt_body_is_invalid_accept_type_is_invalid() 101 | { 102 | $jwtBody = self::validJwtBody(); 103 | $jwtBody[LtiConstants::DL_DEEP_LINK_SETTINGS]['accept_types'] = []; 104 | 105 | $this->expectException(LtiException::class); 106 | 107 | DeepLinkMessageValidator::validate($jwtBody); 108 | } 109 | 110 | public function test_jwt_body_is_invalid_missing_presentation() 111 | { 112 | $jwtBody = self::validJwtBody(); 113 | unset($jwtBody[LtiConstants::DL_DEEP_LINK_SETTINGS]['accept_presentation_document_targets']); 114 | 115 | $this->expectException(LtiException::class); 116 | 117 | DeepLinkMessageValidator::validate($jwtBody); 118 | } 119 | 120 | private static function validJwtBody() 121 | { 122 | return [ 123 | 'sub' => 'subscriber', 124 | LtiConstants::MESSAGE_TYPE => DeepLinkMessageValidator::getMessageType(), 125 | LtiConstants::VERSION => LtiConstants::V1_3, 126 | LtiConstants::ROLES => [], 127 | LtiConstants::DL_DEEP_LINK_SETTINGS => [ 128 | 'deep_link_return_url' => 'https://example.com', 129 | 'accept_types' => ['ltiResourceLink'], 130 | 'accept_presentation_document_targets' => ['iframe'], 131 | ], 132 | ]; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/MessageValidators/ResourceMessageValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(ResourceMessageValidator::canValidate(self::validJwtBody())); 15 | } 16 | 17 | public function test_it_cannot_validate() 18 | { 19 | $jwtBody = self::validJwtBody(); 20 | $jwtBody[LtiConstants::MESSAGE_TYPE] = 'some other type'; 21 | 22 | $this->assertFalse(ResourceMessageValidator::canValidate($jwtBody)); 23 | } 24 | 25 | public function test_jwt_body_is_valid() 26 | { 27 | $this->assertNull(ResourceMessageValidator::validate(self::validJwtBody())); 28 | } 29 | 30 | public function test_jwt_body_is_invalid_missing_sub() 31 | { 32 | $jwtBody = self::validJwtBody(); 33 | $jwtBody['sub'] = ''; 34 | 35 | $this->expectException(LtiException::class); 36 | 37 | ResourceMessageValidator::validate($jwtBody); 38 | } 39 | 40 | public function test_jwt_body_is_invalid_missing_lti_version() 41 | { 42 | $jwtBody = self::validJwtBody(); 43 | unset($jwtBody[LtiConstants::VERSION]); 44 | 45 | $this->expectException(LtiException::class); 46 | 47 | ResourceMessageValidator::validate($jwtBody); 48 | } 49 | 50 | public function test_jwt_body_is_invalid_wrong_lti_version() 51 | { 52 | $jwtBody = self::validJwtBody(); 53 | $jwtBody[LtiConstants::VERSION] = '1.2.0'; 54 | 55 | $this->expectException(LtiException::class); 56 | 57 | ResourceMessageValidator::validate($jwtBody); 58 | } 59 | 60 | public function test_jwt_body_is_invalid_missing_roles() 61 | { 62 | $jwtBody = self::validJwtBody(); 63 | unset($jwtBody[LtiConstants::ROLES]); 64 | 65 | $this->expectException(LtiException::class); 66 | 67 | ResourceMessageValidator::validate($jwtBody); 68 | } 69 | 70 | public function test_jwt_body_is_invalid_missing_resource_link_id() 71 | { 72 | $jwtBody = self::validJwtBody(); 73 | unset($jwtBody[LtiConstants::RESOURCE_LINK]['id']); 74 | 75 | $this->expectException(LtiException::class); 76 | 77 | ResourceMessageValidator::validate($jwtBody); 78 | } 79 | 80 | private static function validJwtBody() 81 | { 82 | return [ 83 | 'sub' => 'subscriber', 84 | LtiConstants::MESSAGE_TYPE => ResourceMessageValidator::getMessageType(), 85 | LtiConstants::VERSION => LtiConstants::V1_3, 86 | LtiConstants::ROLES => [], 87 | LtiConstants::RESOURCE_LINK => [ 88 | 'id' => 'unique-id', 89 | ], 90 | ]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/MessageValidators/SubmissionReviewMessageValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(SubmissionReviewMessageValidator::canValidate(self::validJwtBody())); 15 | } 16 | 17 | public function test_it_cannot_validate() 18 | { 19 | $jwtBody = self::validJwtBody(); 20 | $jwtBody[LtiConstants::MESSAGE_TYPE] = 'some other type'; 21 | 22 | $this->assertFalse(SubmissionReviewMessageValidator::canValidate($jwtBody)); 23 | } 24 | 25 | public function test_jwt_body_is_valid() 26 | { 27 | $this->assertNull(SubmissionReviewMessageValidator::validate(self::validJwtBody())); 28 | } 29 | 30 | public function test_jwt_body_is_invalid_missing_sub() 31 | { 32 | $jwtBody = self::validJwtBody(); 33 | $jwtBody['sub'] = ''; 34 | 35 | $this->expectException(LtiException::class); 36 | 37 | SubmissionReviewMessageValidator::validate($jwtBody); 38 | } 39 | 40 | public function test_jwt_body_is_invalid_missing_lti_version() 41 | { 42 | $jwtBody = self::validJwtBody(); 43 | unset($jwtBody[LtiConstants::VERSION]); 44 | 45 | $this->expectException(LtiException::class); 46 | 47 | SubmissionReviewMessageValidator::validate($jwtBody); 48 | } 49 | 50 | public function test_jwt_body_is_invalid_wrong_lti_version() 51 | { 52 | $jwtBody = self::validJwtBody(); 53 | $jwtBody[LtiConstants::VERSION] = '1.2.0'; 54 | 55 | $this->expectException(LtiException::class); 56 | 57 | SubmissionReviewMessageValidator::validate($jwtBody); 58 | } 59 | 60 | public function test_jwt_body_is_invalid_missing_roles() 61 | { 62 | $jwtBody = self::validJwtBody(); 63 | unset($jwtBody[LtiConstants::ROLES]); 64 | 65 | $this->expectException(LtiException::class); 66 | 67 | SubmissionReviewMessageValidator::validate($jwtBody); 68 | } 69 | 70 | public function test_jwt_body_is_invalid_missing_resource_link_id() 71 | { 72 | $jwtBody = self::validJwtBody(); 73 | unset($jwtBody[LtiConstants::RESOURCE_LINK]['id']); 74 | 75 | $this->expectException(LtiException::class); 76 | 77 | SubmissionReviewMessageValidator::validate($jwtBody); 78 | } 79 | 80 | public function test_jwt_body_is_invalid_missing_for_user() 81 | { 82 | $jwtBody = self::validJwtBody(); 83 | unset($jwtBody[LtiConstants::FOR_USER]); 84 | 85 | $this->expectException(LtiException::class); 86 | 87 | SubmissionReviewMessageValidator::validate($jwtBody); 88 | } 89 | 90 | private static function validJwtBody() 91 | { 92 | return [ 93 | 'sub' => 'subscriber', 94 | LtiConstants::MESSAGE_TYPE => SubmissionReviewMessageValidator::getMessageType(), 95 | LtiConstants::VERSION => LtiConstants::V1_3, 96 | LtiConstants::ROLES => [], 97 | LtiConstants::RESOURCE_LINK => [ 98 | 'id' => 'unique-id', 99 | ], 100 | LtiConstants::FOR_USER => 'user', 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/ServiceRequestTest.php: -------------------------------------------------------------------------------- 1 | request = new ServiceRequest($this->method, $this->url, $this->type); 17 | } 18 | 19 | public function test_it_instantiates() 20 | { 21 | $this->assertInstanceOf(ServiceRequest::class, $this->request); 22 | } 23 | 24 | public function test_it_gets_url() 25 | { 26 | $result = $this->request->getUrl(); 27 | 28 | $this->assertEquals($this->url, $result); 29 | } 30 | 31 | public function test_it_sets_url() 32 | { 33 | $expected = 'http://example.com/foo/bar'; 34 | 35 | $this->request->setUrl($expected); 36 | 37 | $this->assertEquals($expected, $this->request->getUrl()); 38 | } 39 | 40 | public function test_it_gets_payload() 41 | { 42 | $expected = [ 43 | 'headers' => [ 44 | 'Accept' => 'application/json', 45 | ], 46 | ]; 47 | 48 | $this->assertEquals($expected, $this->request->getPayload()); 49 | } 50 | 51 | public function test_it_sets_access_token() 52 | { 53 | $expected = [ 54 | 'headers' => [ 55 | 'Authorization' => 'Bearer foo-bar', 56 | 'Accept' => 'application/json', 57 | ], 58 | ]; 59 | 60 | $this->request->setAccessToken('foo-bar'); 61 | 62 | $this->assertEquals($expected, $this->request->getPayload()); 63 | } 64 | 65 | public function test_it_sets_content_type() 66 | { 67 | $expected = [ 68 | 'headers' => [ 69 | 'Content-Type' => 'foo-bar', 70 | 'Accept' => 'application/json', 71 | ], 72 | ]; 73 | 74 | $request = new ServiceRequest(ServiceRequest::METHOD_POST, $this->url, $this->type); 75 | $request->setContentType('foo-bar'); 76 | 77 | $this->assertEquals($expected, $request->getPayload()); 78 | } 79 | 80 | public function test_it_sets_body() 81 | { 82 | $expected = [ 83 | 'headers' => [ 84 | 'Accept' => 'application/json', 85 | ], 86 | 'body' => 'foo-bar', 87 | ]; 88 | 89 | $this->request->setBody('foo-bar'); 90 | 91 | $this->assertEquals($expected, $this->request->getPayload()); 92 | } 93 | 94 | public function test_it_gets_mask_response_logs() 95 | { 96 | $this->assertFalse($this->request->getMaskResponseLogs()); 97 | } 98 | 99 | public function test_it_sets_mask_response_logs() 100 | { 101 | $this->request->setMaskResponseLogs(true); 102 | 103 | $this->assertTrue($this->request->getMaskResponseLogs()); 104 | } 105 | 106 | public function test_it_gets_error_prefix() 107 | { 108 | $this->assertEquals('Authenticating:', $this->request->getErrorPrefix()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 |