diff options
author | Thomas Steur <tsteur@users.noreply.github.com> | 2018-12-06 08:44:01 +0300 |
---|---|---|
committer | diosmosis <diosmosis@users.noreply.github.com> | 2018-12-06 08:44:01 +0300 |
commit | 7b6a14626960795b0acdf4a064cd3982d7704f2b (patch) | |
tree | 1f8b5fc7d7869c77e91a1f8dc2ab592b3f8de4ca /plugins/CoreAdminHome/tests | |
parent | 1fe8a6ee582c2f61dc209b7df856695378007c23 (diff) |
Report tracking into wrong Site ID and missing token auth (#13493)
* log tracking failures
* added page
* tweak UI
* use a db column instead of option table to simplify code
* add system summary, notifiy super users by email, fixes, update, ...
* more fixes, needs tests next
* add widget for tracking failures
* ensure to not log any failure when visit is excluded
* some tests and fixes
* added tests
* added missing test
* apply review feedback
* fix tests
* trying to fix test
* fix tests
* fix update names
* fix tests
* Fix another test.
Diffstat (limited to 'plugins/CoreAdminHome/tests')
15 files changed, 500 insertions, 1 deletions
diff --git a/plugins/CoreAdminHome/tests/Fixture/TrackingFailures.php b/plugins/CoreAdminHome/tests/Fixture/TrackingFailures.php new file mode 100644 index 0000000000..f8ab0cb808 --- /dev/null +++ b/plugins/CoreAdminHome/tests/Fixture/TrackingFailures.php @@ -0,0 +1,46 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Plugins\CoreAdminHome\tests\Fixture; + +use Piwik\Date; +use Piwik\Tests\Framework\Fixture; + +class TrackingFailures extends Fixture +{ + public $idSite = 1; + public $dateTime = '2013-01-02 03:04:05'; + + public function setUp() + { + parent::setUp(); + + Fixture::createSuperUser(); + if (!self::siteCreated($this->idSite)) { + Fixture::createWebsite('2014-01-02 03:04:05'); + } + $this->trackData(); + } + + private function trackData() + { + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + self::checkResponse($t->doTrackPageView('Valid Site')); + + $t = self::getTracker(99999, Date::now()->getDatetime(), $defaultInit = true); + + for ($i = 0; $i < 2; $i++) { + // we trigger it multiple times to test it will be inserted only once + $t->doTrackPageView('Invalid Site'); + } + + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + $t->setIp('10.11.12.13'); + $t->setTokenAuth('foobar'); // wrong token + $t->doTrackPageView('Invalid Token'); + } +}
\ No newline at end of file diff --git a/plugins/CoreAdminHome/tests/Fixtures/SimpleFixtureTrackFewVisits.php b/plugins/CoreAdminHome/tests/Fixtures/SimpleFixtureTrackFewVisits.php new file mode 100644 index 0000000000..b5c7fcf711 --- /dev/null +++ b/plugins/CoreAdminHome/tests/Fixtures/SimpleFixtureTrackFewVisits.php @@ -0,0 +1,77 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Plugins\CoreAdminHome\tests\Fixtures; + +use Piwik\Date; +use Piwik\Tests\Framework\Fixture; + +/** + * Generates tracker testing data for our TrackingFailuresTest + * + * This Simple fixture adds one website and tracks one visit with couple pageviews and an ecommerce conversion + */ +class SimpleFixtureTrackFewVisits extends Fixture +{ + public $dateTime = '2013-01-23 01:23:45'; + public $idSite = 1; + + public function setUp() + { + $this->setUpWebsite(); + $this->trackFirstVisit(); + $this->trackSecondVisit(); + } + + public function tearDown() + { + // empty + } + + private function setUpWebsite() + { + if (!self::siteCreated($this->idSite)) { + $idSite = self::createWebsite($this->dateTime, $ecommerce = 1); + $this->assertSame($this->idSite, $idSite); + } + } + + protected function trackFirstVisit() + { + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime()); + $t->setUrl('http://example.com/'); + self::checkResponse($t->doTrackPageView('Viewing homepage')); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.2)->getDatetime()); + $t->setUrl('http://example.com/sub/page'); + self::checkResponse($t->doTrackPageView('Second page view')); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.25)->getDatetime()); + $t->addEcommerceItem($sku = 'SKU_ID', $name = 'Test item!', $category = 'Test & Category', $price = 777, $quantity = 33); + self::checkResponse($t->doTrackEcommerceOrder('TestingOrder', $grandTotal = 33 * 77)); + } + + protected function trackSecondVisit() + { + $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); + $t->setIp('56.11.55.73'); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime()); + $t->setUrl('http://example.com/sub/page'); + self::checkResponse($t->doTrackPageView('Viewing homepage')); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.2)->getDatetime()); + $t->setUrl('http://example.com/?search=this is a site search query'); + self::checkResponse($t->doTrackPageView('Site search query')); + + $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.3)->getDatetime()); + $t->addEcommerceItem($sku = 'SKU_ID2', $name = 'A durable item', $category = 'Best seller', $price = 321); + self::checkResponse($t->doTrackEcommerceCartUpdate($grandTotal = 33 * 77)); + } +}
\ No newline at end of file diff --git a/plugins/CoreAdminHome/tests/Integration/APITest.php b/plugins/CoreAdminHome/tests/Integration/APITest.php new file mode 100644 index 0000000000..a3ddf2a182 --- /dev/null +++ b/plugins/CoreAdminHome/tests/Integration/APITest.php @@ -0,0 +1,134 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\CoreAdminHome\tests\Integration; + +use Piwik\Plugins\CoreAdminHome\API; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\Mock\FakeAccess; + +/** + * @group CoreAdminHome + * @group APITest + * @group API + * @group Plugins + */ +class APITest extends \Piwik\Tests\Framework\TestCase\IntegrationTestCase +{ + /** + * @var int + */ + private $idSite; + + /** + * @var API + */ + private $api; + + public function setUp() + { + parent::setUp(); + $this->api = API::getInstance(); + for ($i = 0; $i < 5; $i++) { + Fixture::createWebsite('2014-01-02 03:04:05'); + } + } + + /** + * @expectedException \Piwik\NoAccessException + * @expectedExceptionMessage checkUserHasSomeAdminAccess + */ + public function test_getTrackingFailures_failsForViewUser() + { + $this->setUser(); + $this->api->getTrackingFailures(); + } + + public function test_getTrackingFailures_WorksForAdminAndSuperuser() + { + $this->setAdminUser(); + $this->assertSame(array(), $this->api->getTrackingFailures()); + $this->setSuperUser(); + $this->api->getTrackingFailures(); + $this->assertSame(array(), $this->api->getTrackingFailures()); + } + + /** + * @expectedException \Piwik\NoAccessException + * @expectedExceptionMessage checkUserHasSomeAdminAccess + */ + public function test_deleteAllTrackingFailures_failsForViewUser() + { + $this->setUser(); + $this->api->deleteAllTrackingFailures(); + } + + public function test_deleteAllTrackingFailures_WorksForAdminAndSuperuser() + { + $this->setAdminUser(); + $this->api->deleteAllTrackingFailures(); + $this->setSuperUser(); + $this->api->deleteAllTrackingFailures(); + } + + /** + * @expectedException \Piwik\NoAccessException + * @expectedExceptionMessage checkUserHasAdminAccess + */ + public function test_deleteTrackingFailure_failsForViewUser() + { + $this->setUser(); + $this->api->deleteTrackingFailure(1, 2); + } + + /** + * @expectedException \Piwik\NoAccessException + * @expectedExceptionMessage checkUserHasAdminAccess + */ + public function test_deleteTrackingFailure_failsForAdminUserIfNotAdminAccessToThatSite() + { + $this->setAdminUser(); + $this->api->deleteTrackingFailure(2, 2); + } + + public function test_deleteTrackingFailure_WorksForAdminAndSuperuser() + { + $this->setAdminUser(); + $this->api->deleteTrackingFailure(1, 2); + $this->setSuperUser(); + $this->api->deleteTrackingFailure(1, 2); + } + + protected function setSuperUser() + { + FakeAccess::clearAccess(true); + } + + protected function setUser() + { + FakeAccess::clearAccess(false); + FakeAccess::$identity = 'testUser'; + FakeAccess::$idSitesView = array(1,3, $this->idSite); + FakeAccess::$idSitesAdmin = array(); + } + + protected function setAdminUser() + { + FakeAccess::clearAccess(false); + FakeAccess::$identity = 'testUser'; + FakeAccess::$idSitesView = array(); + FakeAccess::$idSitesAdmin = array(1,3, $this->idSite); + } + + public function provideContainerConfig() + { + return array( + 'Piwik\Access' => new FakeAccess() + ); + } +} diff --git a/plugins/CoreAdminHome/tests/Integration/TasksTest.php b/plugins/CoreAdminHome/tests/Integration/TasksTest.php index d3ffc66518..ffab94c01d 100644 --- a/plugins/CoreAdminHome/tests/Integration/TasksTest.php +++ b/plugins/CoreAdminHome/tests/Integration/TasksTest.php @@ -14,12 +14,15 @@ use Piwik\Date; use Piwik\Db; use Piwik\Mail; use Piwik\Plugins\CoreAdminHome\Emails\JsTrackingCodeMissingEmail; +use Piwik\Plugins\CoreAdminHome\Emails\TrackingFailuresEmail; use Piwik\Plugins\CoreAdminHome\Tasks; use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList; use Piwik\Scheduler\Task; use Piwik\Tests\Fixtures\RawArchiveDataWithTempAndInvalidated; use Piwik\Tests\Framework\Fixture; use Piwik\Tests\Framework\TestCase\IntegrationTestCase; +use Piwik\Tracker\Failures; +use Piwik\Tracker\Request; use Psr\Log\NullLogger; /** @@ -66,7 +69,7 @@ class TasksTest extends IntegrationTestCase $archivePurger->setYesterdayDate(Date::factory('2015-02-26')); $archivePurger->setNow(Date::factory('2015-02-27 08:00:00')->getTimestamp()); - $this->tasks = new Tasks($archivePurger, new NullLogger()); + $this->tasks = new Tasks($archivePurger, new NullLogger(), new Failures()); $this->mail = null; } @@ -129,6 +132,8 @@ class TasksTest extends IntegrationTestCase 'purgeOutdatedArchives.', 'purgeInvalidatedArchives.', 'optimizeArchiveTable.', + 'cleanupTrackingFailures.', + 'notifyTrackingFailures.', 'updateSpammerBlacklist.', 'checkSiteHasTrackedVisits.2', 'checkSiteHasTrackedVisits.3', @@ -181,6 +186,35 @@ class TasksTest extends IntegrationTestCase $this->assertEquals($mail->getIdSite(), $idSite); } + public function test_cleanupTrackingFailures_doesNotCauseAnyException() + { + // it is only calling one method which is already tested... no need to write complex tests for it + $this->tasks->cleanupTrackingFailures(); + $this->assertTrue(true); + } + + public function test_notifyTrackingFailures_doesNotSendAnyMailWhenThereAreNoTrackingRequests() + { + $this->tasks->notifyTrackingFailures(); + $this->assertNull($this->mail); + } + + public function test_notifyTrackingFailures_sendsMailWhenThereAreTrackingFailures() + { + $failures = new Failures(); + $failures->logFailure(1, new Request(array('idsite' => 9999, 'rec' => 1))); + $failures->logFailure(1, new Request(array('idsite' => 9998, 'rec' => 1))); + Fixture::createSuperUser(false); + $this->tasks->notifyTrackingFailures(); + + /** @var TrackingFailuresEmail $mail */ + $mail = $this->mail; + $this->assertInstanceOf(TrackingFailuresEmail::class, $mail); + $this->assertEquals('superUserLogin', $mail->getLogin()); + $this->assertEquals('hello@example.org', $mail->getEmailAddress()); + $this->assertEquals(2, $mail->getNumFailures()); + } + /** * @param Date[] $dates */ diff --git a/plugins/CoreAdminHome/tests/System/TrackingFailuresTest.php b/plugins/CoreAdminHome/tests/System/TrackingFailuresTest.php new file mode 100644 index 0000000000..061c819e60 --- /dev/null +++ b/plugins/CoreAdminHome/tests/System/TrackingFailuresTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\CoreAdminHome\tests\System; + +use Piwik\Plugins\CoreAdminHome\tests\Fixture\TrackingFailures; +use Piwik\Tests\Framework\TestCase\SystemTestCase; + +/** + * @group CoreAdminHome + * @group TrackingFailuresTest + * @group Plugins + */ +class TrackingFailuresTest extends SystemTestCase +{ + /** + * @var TrackingFailures + */ + public static $fixture = null; // initialized below class definition + + /** + * @dataProvider getApiForTesting + */ + public function testApi($api, $params) + { + $params['xmlFieldsToRemove'] = array('date_first_occurred', 'pretty_date_first_occurred', 'request_url'); + $this->runApiTests($api, $params); + } + + public function getApiForTesting() + { + $api = array( + 'CoreAdminHome.getTrackingFailures', + ); + + $apiToTest = array(); + $apiToTest[] = array($api, + array( + 'testSuffix' => '' + ) + ); + + return $apiToTest; + } + + public static function getOutputPrefix() + { + return ''; + } + + public static function getPathToTestDirectory() + { + return dirname(__FILE__); + } + +} + +TrackingFailuresTest::$fixture = new TrackingFailures();
\ No newline at end of file diff --git a/plugins/CoreAdminHome/tests/System/expected/test___CoreAdminHome.getTrackingFailures.xml b/plugins/CoreAdminHome/tests/System/expected/test___CoreAdminHome.getTrackingFailures.xml new file mode 100644 index 0000000000..511339901f --- /dev/null +++ b/plugins/CoreAdminHome/tests/System/expected/test___CoreAdminHome.getTrackingFailures.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <idsite>1</idsite> + <idfailure>2</idfailure> + + + <site_name>Piwik test</site_name> + + <url>http://example.com/piwik/</url> + <problem>Request was not authenticated but authentication was required.</problem> + <solution>Set or correct a "token_auth" in your tracking request.</solution> + <solution_url>https://matomo.org/faq/how-to/faq_30835/</solution_url> + </row> + <row> + <idsite>99999</idsite> + <idfailure>1</idfailure> + + + <site_name>Unknown</site_name> + + <url>http://example.com/piwik/</url> + <problem>The site does not exist.</problem> + <solution>Update the configured idSite in the tracker.</solution> + <solution_url>https://matomo.org/faq/how-to/faq_30838/</solution_url> + </row> +</result>
\ No newline at end of file diff --git a/plugins/CoreAdminHome/tests/UI/TrackingFailures_spec.js b/plugins/CoreAdminHome/tests/UI/TrackingFailures_spec.js new file mode 100644 index 0000000000..f98f1c0646 --- /dev/null +++ b/plugins/CoreAdminHome/tests/UI/TrackingFailures_spec.js @@ -0,0 +1,94 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe("TrackingFailures", function () { + this.timeout(0); + + var manageUrl = '?module=CoreAdminHome&action=trackingFailures&idSite=1&period=day&date=today'; + var widgetUrl = '?module=Widgetize&action=iframe&moduleToWidgetize=CoreAdminHome&actionToWidgetize=getTrackingFailures&idSite=1&period=day&date=today&widget=1'; + + function captureScreen(done, screenshotName, theTest) + { + expect.screenshot(screenshotName).to.be.captureSelector('.matomoTrackingFailures', theTest, done); + } + + function captureModal(done, screenshotName, theTest) + { + expect.screenshot(screenshotName).to.be.captureSelector('.modal.open', theTest, done); + } + + function generateTrackingFailures() + { + testEnvironment.generateTrackingFailures = 1; + testEnvironment.save(); + } + + function confirmModal(page) + { + page.click('.modal.open .modal-footer a:contains(Yes)'); + } + + afterEach(function () { + delete testEnvironment.generateTrackingFailures; + testEnvironment.save(); + }); + + it('should show widget with no failures', function (done) { + captureScreen(done, 'widget_no_failures', function (page) { + page.load(widgetUrl); + }); + }); + + it('should show manage page with no failures', function (done) { + captureScreen(done, 'manage_no_failures', function (page) { + page.load(manageUrl); + }); + }); + + it('should show widget with failures', function (done) { + generateTrackingFailures(); + captureScreen(done, 'widget_with_failures', function (page) { + generateTrackingFailures(); + page.load(widgetUrl); + }); + }); + + it('should show manage page with failures', function (done) { + generateTrackingFailures(); + captureScreen(done, 'manage_with_failures', function (page) { + generateTrackingFailures(); + page.load(manageUrl); + }); + }); + + it('should show ask to confirm delete one', function (done) { + captureModal(done, 'manage_with_failures_delete_one_ask_confirmation', function (page) { + page.evaluate(function () { + $('.matomoTrackingFailures table tbody tr:nth-child(2) .icon-delete').click() + }); + }); + }); + + it('should show delete when confirmed', function (done) { + captureScreen(done, 'manage_with_failures_delete_one_confirmed', function (page) { + confirmModal(page); + }); + }); + + it('should show ask to confirm delete all', function (done) { + captureModal(done, 'manage_with_failures_delete_all_ask_confirmation', function (page) { + page.click('.matomoTrackingFailures .deleteAllFailures'); + }); + }); + + it('should show ask to confirm delete one', function (done) { + captureScreen(done, 'manage_with_failures_delete_all_confirmed', function (page) { + confirmModal(page); + }); + }); + +});
\ No newline at end of file diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_no_failures.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_no_failures.png new file mode 100644 index 0000000000..3652d932ca --- /dev/null +++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_no_failures.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:555956386a3960079e98fd0ed955ccf836d5aef1e9883f330ef08e43c496f3ff +size 25706 diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures.png new file mode 100644 index 0000000000..3920b896ea --- /dev/null +++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:164fc892e74d90ff316f76cc424dbb00e8af9f8ff5c487deaca4df67fb3b058d +size 75105 diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_ask_confirmation.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_ask_confirmation.png new file mode 100644 index 0000000000..d087655199 --- /dev/null +++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_ask_confirmation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a8c736bf8efa2dde7c14609ae473fddd4c09f616cbfa1d794bddcc01d12b707 +size 9554 diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png new file mode 100644 index 0000000000..3652d932ca --- /dev/null +++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:555956386a3960079e98fd0ed955ccf836d5aef1e9883f330ef08e43c496f3ff +size 25706 diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_ask_confirmation.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_ask_confirmation.png new file mode 100644 index 0000000000..372005bf86 --- /dev/null +++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_ask_confirmation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a773a094fe4a81c031a71ad279ccda03cee6f3b8139ed6bbaac27c6ea8ff7c4 +size 9585 diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_confirmed.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_confirmed.png new file mode 100644 index 0000000000..237b4c9a7a --- /dev/null +++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_one_confirmed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32619750eb571199b8fb5b0761efe64d46f9db3dd090b7f8bbc7a1151aa5e295 +size 48513 diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_no_failures.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_no_failures.png new file mode 100644 index 0000000000..c610efccb2 --- /dev/null +++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_no_failures.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8151ae81cc83466e763346f02b272dc7ec480832d49807f59cfb32a53630da6a +size 11165 diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_with_failures.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_with_failures.png new file mode 100644 index 0000000000..fc40fbe9b1 --- /dev/null +++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_widget_with_failures.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a98049c33bb5cab09436985b5cf062a931560028014f8c9f8b1ccc0d50c3af8 +size 12805 |