'use strict'; const ipReveal = require( 'ext.checkUser.tempAccounts/ipReveal.js' ); const { makeTempUserLink, waitUntilElementDisappears } = require( './utils.js' ); const ipRevealUtils = require( 'ext.checkUser.tempAccounts/ipRevealUtils.js' ); const originalGetAutoRevealStatus = ipRevealUtils.getAutoRevealStatus; let server; // Define user names that can be used in these tests, which are removed after each test run. const tempName1 = '~1'; const tempName2 = '~2'; const tempName3 = '~3'; QUnit.module( 'ext.checkUser.tempAccounts.ipReveal', QUnit.newMwEnvironment( { beforeEach: function () { this.server = this.sandbox.useFakeServer(); this.server.respondImmediately = true; server = this.server; }, afterEach: function () { server.restore(); ipRevealUtils.getAutoRevealStatus = originalGetAutoRevealStatus; // Ensure no users are set to pre-revealed mw.storage.remove( 'mw-checkuser-temp-' + tempName1 ); mw.storage.remove( 'mw-checkuser-temp-' + tempName2 ); mw.storage.remove( 'mw-checkuser-temp-' + tempName3 ); }, config: { // Prevent initOnLoad.js from running automatically and then // calling enableMultiReveal on the document. wgCanonicalSpecialPageName: 'CheckUser' } } ) ); QUnit.test( 'Test getRevisionId', ( assert ) => { // Create an element with a child that has the data-mw-revid attribute. const elementWithRevId = document.createElement( 'div' ); const childWithRevId = document.createElement( 'span' ); childWithRevId.setAttribute( 'data-mw-revid', '123456' ); elementWithRevId.appendChild( childWithRevId ); // Call getRevisionId with the newly created element, to test when the // element is a revision line. assert.strictEqual( ipReveal.getRevisionId( $( childWithRevId ) ), 123456, 'getRevisionId return value for element with data-mw-revid attribute' ); // Call getRevisionId with an element that does not have data-mw-revid, to test // when the element is not a revision line. assert.strictEqual( ipReveal.getRevisionId( $( document.createElement( 'div' ) ) ), undefined, 'getRevisionId return value for element without data-mw-revid attribute' ); } ); QUnit.test( 'Test getLogId', ( assert ) => { // Create an element with a child that has the data-mw-logid attribute. const elementWithRevId = document.createElement( 'div' ); const childWithRevId = document.createElement( 'span' ); childWithRevId.setAttribute( 'data-mw-logid', '123456' ); elementWithRevId.appendChild( childWithRevId ); // Call getLogId with the newly created element, to test when the // element is a log line. assert.strictEqual( ipReveal.getLogId( $( childWithRevId ) ), 123456, 'getLogId return value for element with data-mw-logid attribute' ); // Call getLogId with an element that does not have data-mw-logid, to test // when the element is not a log line. assert.strictEqual( ipReveal.getLogId( $( document.createElement( 'div' ) ) ), undefined, 'getLogId return value for element without data-mw-logid attribute' ); } ); QUnit.test( 'Test enableMultiReveal', ( assert ) => { // eslint-disable-next-line no-jquery/no-global-selector const $qunitFixture = $( '#qunit-fixture' ); // Add some testing revision lines to test the multi reveal functionality. const revisionLines = { 1: tempName1, 2: tempName1, 3: tempName2 }; Object.entries( revisionLines ).forEach( ( [ revId, username ] ) => { const $revisionLine = $( '
' ).attr( 'data-mw-revid', revId ); $qunitFixture.append( $revisionLine ); // Add the temporary account username link $revisionLine.append( makeTempUserLink( username ) ); // Add the Show IP button manually for the temporary account username link. $revisionLine.append( ipReveal.makeButton( username, { targetId: revId, allIds: [ revId ] }, {}, {}, $qunitFixture ) ); assert.strictEqual( $revisionLine.find( '.ext-checkuser-tempaccount-reveal-ip-button' ).text(), '(checkuser-tempaccount-reveal-ip-button-label)', 'IP reveal button for revId ' + revId + ' before multi reveal' ); } ); // Enable multi reveal on the QUnit fixture. ipReveal.enableMultiReveal( $qunitFixture ); // Fire the userRevealed event with the ~1 temporary account username // and the IP addresses for the revisions. const isRevisionLookup = true; const isLogLookup = false; const isAbuseFilterLookup = false; const ips = { 1: '127.0.0.1', 2: '127.0.0.2', 3: '127.0.0.3' }; const batchResponse = { [ tempName1 ]: { revIps: ips, expired: { revIds: [], logIps: [] } } }; $qunitFixture.trigger( 'userRevealed', [ tempName1, ips, isRevisionLookup, isLogLookup, isAbuseFilterLookup, batchResponse ] ); // Check that the IP addresses were added to the buttons for the revisions // with the ~1 temporary account username. assert.strictEqual( $qunitFixture.find( '[data-mw-revid="1"] .ext-checkuser-tempaccount-reveal-ip' ).text(), '127.0.0.1', 'IP reveal button for revId 1 before multi reveal' ); assert.strictEqual( $qunitFixture.find( '[data-mw-revid="2"] .ext-checkuser-tempaccount-reveal-ip' ).text(), '127.0.0.2', 'IP reveal button for revId 2 before multi reveal' ); assert.strictEqual( $qunitFixture.find( '[data-mw-revid="3"] .ext-checkuser-tempaccount-reveal-ip-button' ).text(), '(checkuser-tempaccount-reveal-ip-button-label)', 'IP reveal button for revId 3 before multi reveal' ); } ); QUnit.test( 'Test enableMultiReveal with grouped recent changes', ( assert ) => { const done = assert.async(); server.respond( ( request ) => { if ( !request.url.includes( '/batch-temporaryaccount' ) ) { assert.expect( 0 ); done( new Error( `Unexpected request URL: ${ request.url }` ) ); } request.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( { [ tempName1 ]: { // eslint-disable-next-line quote-props revIps: { '1': '127.0.0.1' }, // eslint-disable-next-line quote-props logIps: { '1': '127.0.0.1' }, // eslint-disable-next-line quote-props abuseLogIps: { '1': '127.0.0.4' }, lastUsedIp: '127.0.0.3' } } ) ); } ); // eslint-disable-next-line no-jquery/no-global-selector const $qunitFixture = $( '#qunit-fixture' ); // Add a revision line and a line with no ID for the same user, mimicking grouped recent changes const lines = [ { $element: $( '
' ).attr( 'data-mw-revid', 1 ), revIds: { targetId: 1, allIds: [ 1 ] }, logIds: {}, aflIds: {} }, { $element: $( '
' ).attr( 'data-mw-logid', 1 ), revIds: {}, logIds: { targetId: 1, allIds: [ 1 ] }, aflIds: {} }, { $element: $( '
' ).attr( 'data-afl-log-id', 1 ), revIds: {}, logIds: {}, aflIds: { targetId: 1, allIds: [ 1 ] } }, { $element: $( '
' ).addClass( 'no-id' ), revIds: {}, logIds: {}, aflIds: {} } ]; lines.forEach( ( line ) => { const username = tempName1; $qunitFixture.append( line.$element ); // Add the temporary account username link line.$element.append( makeTempUserLink( username ) ); // Add the Show IP button manually for the temporary account username link. line.$element.append( ipReveal.makeButton( username, line.revIds, line.logIds, line.aflIds, $qunitFixture ) ); } ); // Enable multi reveal on the QUnit fixture. ipReveal.enableMultiReveal( $qunitFixture ); $( '.no-id .ext-checkuser-tempaccount-reveal-ip-button a', $qunitFixture )[ 0 ].click(); waitUntilElementDisappears( '.no-id .ext-checkuser-tempaccount-reveal-ip-button' ).then( () => { // Verify that the button has gone and was replaced with the IP assert.strictEqual( $( '.no-id .ext-checkuser-tempaccount-reveal-ip-button', $qunitFixture ).length, 0, 'Button removed after click' ); assert.strictEqual( $( '.ext-checkuser-tempaccount-reveal-ip', $qunitFixture ).text(), '127.0.0.3', 'Text of element that replaced button' ); waitUntilElementDisappears( '[data-mw-revid="1"] .ext-checkuser-tempaccount-reveal-ip-button' ).then( () => { // Verify that the button has gone and was replaced with the IP assert.strictEqual( $( '[data-mw-revid="1"] .ext-checkuser-tempaccount-reveal-ip-button', $qunitFixture ).length, 0, 'Button removed after click' ); assert.strictEqual( $( '[data-mw-revid="1"] .ext-checkuser-tempaccount-reveal-ip', $qunitFixture ).text(), '127.0.0.1', 'Text of element that replaced button' ); waitUntilElementDisappears( '[data-mw-logid="1"] .ext-checkuser-tempaccount-reveal-ip-button' ).then( () => { // Verify that the button has gone and was replaced with the IP assert.strictEqual( $( '[data-mw-logid="1"] .ext-checkuser-tempaccount-reveal-ip-button', $qunitFixture ).length, 0, 'Button removed after click' ); assert.strictEqual( $( '[data-mw-logid="1"] .ext-checkuser-tempaccount-reveal-ip', $qunitFixture ).text(), '127.0.0.1', 'Text of element that replaced button' ); done(); } ); } ); } ); } ); QUnit.test( 'Test addIpRevealButtons adds temporary account IP reveal buttons', ( assert ) => { // eslint-disable-next-line no-jquery/no-global-selector const $qunitFixture = $( '#qunit-fixture' ); const temporaryAccountUserLinks = []; // Add some testing revision lines const revisionLines = { 1: tempName1, 2: tempName1, 3: tempName2 }; Object.entries( revisionLines ).forEach( ( [ revId, username ] ) => { const $revisionLine = $( '
' ).attr( 'data-mw-revid', revId ); $qunitFixture.append( $revisionLine ); // Add the temporary account username link for the revision line const $tempAccountUserLink = makeTempUserLink( username ); $revisionLine.append( $tempAccountUserLink ); temporaryAccountUserLinks.push( $tempAccountUserLink ); } ); // Add some testing log lines const logLines = { 2: tempName1, 5: tempName2, 6: tempName3 }; Object.entries( logLines ).forEach( ( [ logId, username ] ) => { const $logLine = $( '
' ).attr( 'data-mw-logid', logId ); $qunitFixture.append( $logLine ); // Add the temporary account username link for the log line const $tempAccountUserLink = makeTempUserLink( username ); $logLine.append( $tempAccountUserLink ); temporaryAccountUserLinks.push( $tempAccountUserLink ); } ); // Add a temporary account username that is not associated with a revision or log const $tempAccountUserLink = makeTempUserLink( tempName3 ); $qunitFixture.append( $tempAccountUserLink ); temporaryAccountUserLinks.push( $tempAccountUserLink ); // Verify that before the call to ::addIpRevealButtons there are no Show IP buttons assert.strictEqual( $qunitFixture.find( '.ext-checkuser-tempaccount-reveal-ip-button' ).length, 0, 'No IP reveal links before addIpRevealButtons call' ); // Call addIpRevealButtons on the QUnit fixture ipReveal.addIpRevealButtons( $qunitFixture ); // Call again to ensure that the buttons can only be added once ipReveal.addIpRevealButtons( $qunitFixture ); // Verify that the Show IP button was added for all temporary user links temporaryAccountUserLinks.forEach( ( $element ) => { assert.strictEqual( // eslint-disable-next-line no-jquery/no-class-state $element.next().hasClass( 'ext-checkuser-tempaccount-reveal-ip-button' ), true, 'IP reveal button is directly after temporary account user link after addIpRevealButtons call' ); assert.strictEqual( // eslint-disable-next-line no-jquery/no-class-state $element.next().next().hasClass( 'ext-checkuser-tempaccount-reveal-ip-button' ), false, 'Only one IP reveal button is added after multiple addIpRevealButtons calls' ); } ); } ); QUnit.test( 'Test makeButton creates expected button', ( assert ) => { // Call the method under test const elements = ipReveal.makeButton( tempName1, { targetId: 1, allIds: [ 1 ] }, {} ); const [ $button ] = elements; assert.strictEqual( elements.length, 1, 'Only the IP reveal button should be returned for non-blocked users' ); // Verify that the button has the expected button text and classes assert.strictEqual( $button.text(), '(checkuser-tempaccount-reveal-ip-button-label)', 'Button text' ); const expectedClasses = [ 'ext-checkuser-tempaccount-reveal-ip-button', 'oo-ui-flaggedElement-progressive', 'oo-ui-buttonElement' ]; expectedClasses.forEach( ( className ) => { assert.strictEqual( // eslint-disable-next-line no-jquery/no-class-state $button.hasClass( className ), true, 'Button has ' + className + ' class' ); } ); } ); QUnit.test( 'Test makeButton creates expected button for blocked performer', ( assert ) => { mw.config.set( 'wgCheckUserIsPerformerBlocked', true ); const elements = ipReveal.makeButton( tempName1, { targetId: 1, allIds: [ 1 ] }, {} ); const [ $button, $blockInfoWidget ] = elements; assert.strictEqual( elements.length, 2, 'Both an IP reveal button and a block details button should be returned for non-blocked users' ); // Verify that the button has the expected button text and classes assert.strictEqual( $button.text(), '(checkuser-tempaccount-reveal-ip-button-label)', 'Button text' ); assert.strictEqual( $blockInfoWidget.find( '[role=button]' ).attr( 'title' ), '(checkuser-tempaccount-reveal-blocked-title)', 'Block info widget title' ); } ); function performMakeButtonRequestTest( assert, responseCode, responseContent, expectedText ) { // We need the test to wait a small amount of time for the click events to finish. const done = assert.async(); server.respond( ( request ) => { request.respond( responseCode, { 'Content-Type': 'application/json' }, responseContent ); } ); // Call the method under test to get a button const $button = ipReveal.makeButton( tempName1, { targetId: 1, allIds: [ 1 ] }, {} ); // Add the button to the QUnit fixture // eslint-disable-next-line no-jquery/no-global-selector const $qunitFixture = $( '#qunit-fixture' ); $qunitFixture.append( $button ); // Click the button $( '.ext-checkuser-tempaccount-reveal-ip-button a', $qunitFixture )[ 0 ].click(); waitUntilElementDisappears( '.ext-checkuser-tempaccount-reveal-ip-button' ).then( () => { // Verify that the button has gone and was replaced with a span containing an error message assert.strictEqual( $( '.ext-checkuser-tempaccount-reveal-ip-button', $qunitFixture ).length, 0, 'Button removed after click' ); assert.strictEqual( $( '.ext-checkuser-tempaccount-reveal-ip', $qunitFixture ).text(), expectedText, 'Text of element that replaced button' ); done(); } ); } QUnit.test( 'Test makeButton on button click for failed request', ( assert ) => { performMakeButtonRequestTest( assert, 500, '', '(checkuser-tempaccount-reveal-ip-error)' ); } ); QUnit.test( 'Test makeButton on button click for successful request but missing data', ( assert ) => { performMakeButtonRequestTest( assert, 200, JSON.stringify( { [ tempName1 ]: { revIps: { // The API returns the revIds as strings. // null indicates the IP is missing from the DB (i.e. that // it was never there in the first place). // eslint-disable-next-line quote-props '1': null }, logIps: null, lastUsedIp: null }, autoReveal: false } ), '(checkuser-tempaccount-reveal-ip-missing)' ); } ); QUnit.test( 'Test makeButton on button click for successful request but expired data', ( assert ) => { performMakeButtonRequestTest( assert, 200, JSON.stringify( { [ tempName1 ]: { revIps: { // The API returns the revIds as strings. // Having the key missing here indicates that the data has // expired. }, logIps: null, lastUsedIp: null }, autoReveal: false } ), '(checkuser-tempaccount-reveal-ip-expired)' ); } ); QUnit.test( 'Test makeButton on button click for successful request with data', ( assert ) => { performMakeButtonRequestTest( assert, 200, JSON.stringify( { [ tempName1 ]: { revIps: { // The API returns the revIds as strings // eslint-disable-next-line quote-props '1': '127.0.0.1' }, logIps: null, lastUsedIp: null }, autoReveal: false } ), '127.0.0.1' ); } ); QUnit.test( 'Test enableAutoReveal replaces buttons with IPs', function ( assert ) { const done = assert.async(); server.respond( ( request ) => { const response = {}; response[ tempName1 ] = { revIps: { 1: '127.0.0.1', 2: '127.0.0.1' }, logIps: null, lastUsedIp: null }; response[ tempName2 ] = { revIps: { 3: '127.0.0.1' }, logIps: null, lastUsedIp: null }; request.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( response ) ); } ); // eslint-disable-next-line no-jquery/no-global-selector const $qunitFixture = $( '#qunit-fixture' ); // Add some revision lines with temporary account links const revisionLines = { 1: tempName1, 2: tempName1, 3: tempName2 }; Object.entries( revisionLines ).forEach( ( [ revId, username ] ) => { const $revisionLine = $( '
' ).attr( 'data-mw-revid', revId ); $qunitFixture.append( $revisionLine ); const $tempAccountUserLink = makeTempUserLink( username ); $revisionLine.append( $tempAccountUserLink ); $revisionLine.append( ipReveal.makeButton( username, { targetId: revId, allIds: [ revId ] }, {}, $qunitFixture ) ); } ); // Check that auto-reveal mode is switched on const expiry = Math.round( Date.now() / 1000 ) + 3600; const utilsMock = this.sandbox.mock( ipRevealUtils ); utilsMock.expects( 'setAutoRevealStatus' ) .once() .withArgs( expiry ) .returns( $.Deferred().resolve() ); // Enable multi-reveal and switch on auto-reveal mode ipReveal.enableAutoReveal( expiry, $qunitFixture ); // Check all IPs are revealed waitUntilElementDisappears( '.ext-checkuser-tempaccount-reveal-ip-button' ).then( () => { assert.strictEqual( $( '.ext-checkuser-tempaccount-reveal-ip-button', $qunitFixture ).length, 0, 'IP reveal buttons removed' ); assert.strictEqual( $( '.ext-checkuser-tempaccount-reveal-ip', $qunitFixture ).length, 3, 'Revealed IPs added' ); done(); } ); } ); QUnit.test( 'Test disableAutoReveal replaces IPs with buttons', function ( assert ) { const done = assert.async(); // eslint-disable-next-line no-jquery/no-global-selector const $qunitFixture = $( '#qunit-fixture' ); // Add some revision lines with revealed IPs const revisionLines = { 1: tempName1, 2: tempName1, 3: tempName2 }; Object.entries( revisionLines ).forEach( ( [ revId, username ] ) => { const $revisionLine = $( '
' ).attr( 'data-mw-revid', revId ); $qunitFixture.append( $revisionLine ); if ( revId !== '3' ) { // Leave one line without an IP, to ensure a button is not added (T404720) const $tempAccountUserLink = makeTempUserLink( username ); const $revealedIp = $( '' ).addClass( 'ext-checkuser-tempaccount-reveal-ip' ).append( $( '' ).addClass( 'ext-checkuser-tempaccount-reveal-ip-anchor' ) ); $revisionLine.append( $tempAccountUserLink, $revealedIp ); } } ); // Check that auto-reveal mode is switched off const utilsMock = this.sandbox.mock( ipRevealUtils ); utilsMock.expects( 'setAutoRevealStatus' ) .once() .withArgs() .returns( $.Deferred().resolve() ); // Check that the IPs are replaced with buttons ipReveal.disableAutoReveal( $qunitFixture ); // Check all IPs are hidden waitUntilElementDisappears( '.ext-checkuser-tempaccount-reveal-ip' ).then( () => { assert.strictEqual( $( '.ext-checkuser-tempaccount-reveal-ip', $qunitFixture ).length, 0, 'Revealed IPs removed' ); assert.strictEqual( $( '.ext-checkuser-tempaccount-reveal-ip-button', $qunitFixture ).length, 2, 'IP reveal buttons added' ); done(); } ); } );