TokenAccessHandlerTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820
  1. const SandboxedModule = require('sandboxed-module')
  2. const path = require('path')
  3. const sinon = require('sinon')
  4. const modulePath = path.join(
  5. __dirname,
  6. '../../../../app/src/Features/TokenAccess/TokenAccessHandler'
  7. )
  8. const { expect } = require('chai')
  9. const { ObjectId } = require('mongodb')
  10. describe('TokenAccessHandler', function () {
  11. beforeEach(function () {
  12. this.token = 'abcdefabcdef'
  13. this.projectId = new ObjectId()
  14. this.project = {
  15. _id: this.projectId,
  16. publicAccesLevel: 'tokenBased',
  17. owner_ref: new ObjectId(),
  18. }
  19. this.userId = new ObjectId()
  20. this.req = {}
  21. this.TokenAccessHandler = SandboxedModule.require(modulePath, {
  22. requires: {
  23. mongodb: { ObjectId },
  24. '../../models/Project': { Project: (this.Project = {}) },
  25. '@overleaf/metrics': (this.Metrics = { inc: sinon.stub() }),
  26. '@overleaf/settings': (this.settings = {}),
  27. '../V1/V1Api': (this.V1Api = {
  28. request: sinon.stub(),
  29. }),
  30. crypto: (this.Crypto = require('crypto')),
  31. '../Analytics/AnalyticsManager': (this.Analytics = {
  32. recordEventForUser: sinon.stub(),
  33. }),
  34. },
  35. })
  36. })
  37. describe('getTokenType', function () {
  38. it('should determine tokens correctly', function () {
  39. const specs = {
  40. abcdefabcdef: 'readOnly',
  41. aaaaaabbbbbb: 'readOnly',
  42. '54325aaaaaa': 'readAndWrite',
  43. '54325aaaaaabbbbbb': 'readAndWrite',
  44. '': null,
  45. abc123def: null,
  46. }
  47. for (const token of Object.keys(specs)) {
  48. expect(this.TokenAccessHandler.getTokenType(token)).to.equal(
  49. specs[token]
  50. )
  51. }
  52. })
  53. })
  54. describe('getProjectByReadOnlyToken', function () {
  55. beforeEach(function () {
  56. this.token = 'abcdefabcdef'
  57. this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
  58. })
  59. it('should get the project', function (done) {
  60. this.TokenAccessHandler.getProjectByReadOnlyToken(
  61. this.token,
  62. (err, project) => {
  63. expect(err).to.not.exist
  64. expect(project).to.exist
  65. expect(this.Project.findOne.callCount).to.equal(1)
  66. done()
  67. }
  68. )
  69. })
  70. })
  71. describe('getProjectByReadAndWriteToken', function () {
  72. beforeEach(function () {
  73. sinon.spy(this.Crypto, 'timingSafeEqual')
  74. this.token = '1234abcdefabcdef'
  75. this.project.tokens = {
  76. readAndWrite: this.token,
  77. readAndWritePrefix: '1234',
  78. }
  79. this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
  80. })
  81. afterEach(function () {
  82. this.Crypto.timingSafeEqual.restore()
  83. })
  84. it('should get the project and do timing-safe comparison', function (done) {
  85. this.TokenAccessHandler.getProjectByReadAndWriteToken(
  86. this.token,
  87. (err, project) => {
  88. expect(err).to.not.exist
  89. expect(project).to.exist
  90. expect(this.Crypto.timingSafeEqual.callCount).to.equal(1)
  91. expect(
  92. this.Crypto.timingSafeEqual.calledWith(Buffer.from(this.token))
  93. ).to.equal(true)
  94. expect(this.Project.findOne.callCount).to.equal(1)
  95. done()
  96. }
  97. )
  98. })
  99. })
  100. describe('addReadOnlyUserToProject', function () {
  101. beforeEach(function () {
  102. this.Project.updateOne = sinon.stub().callsArgWith(2, null)
  103. })
  104. it('should call Project.updateOne', function (done) {
  105. this.TokenAccessHandler.addReadOnlyUserToProject(
  106. this.userId,
  107. this.projectId,
  108. err => {
  109. expect(err).to.not.exist
  110. expect(this.Project.updateOne.callCount).to.equal(1)
  111. expect(
  112. this.Project.updateOne.calledWith({
  113. _id: this.projectId,
  114. })
  115. ).to.equal(true)
  116. expect(
  117. this.Project.updateOne.lastCall.args[1].$addToSet
  118. ).to.have.keys('tokenAccessReadOnly_refs')
  119. sinon.assert.calledWith(
  120. this.Analytics.recordEventForUser,
  121. this.userId,
  122. 'project-joined',
  123. { mode: 'read-only' }
  124. )
  125. done()
  126. }
  127. )
  128. })
  129. it('should not produce an error', function (done) {
  130. this.TokenAccessHandler.addReadOnlyUserToProject(
  131. this.userId,
  132. this.projectId,
  133. err => {
  134. expect(err).to.not.exist
  135. done()
  136. }
  137. )
  138. })
  139. describe('when Project.updateOne produces an error', function () {
  140. beforeEach(function () {
  141. this.Project.updateOne = sinon
  142. .stub()
  143. .callsArgWith(2, new Error('woops'))
  144. })
  145. it('should produce an error', function (done) {
  146. this.TokenAccessHandler.addReadOnlyUserToProject(
  147. this.userId,
  148. this.projectId,
  149. err => {
  150. expect(err).to.exist
  151. done()
  152. }
  153. )
  154. })
  155. })
  156. })
  157. describe('addReadAndWriteUserToProject', function () {
  158. beforeEach(function () {
  159. this.Project.updateOne = sinon.stub().callsArgWith(2, null)
  160. })
  161. it('should call Project.updateOne', function (done) {
  162. this.TokenAccessHandler.addReadAndWriteUserToProject(
  163. this.userId,
  164. this.projectId,
  165. err => {
  166. expect(err).to.not.exist
  167. expect(this.Project.updateOne.callCount).to.equal(1)
  168. expect(
  169. this.Project.updateOne.calledWith({
  170. _id: this.projectId,
  171. })
  172. ).to.equal(true)
  173. expect(
  174. this.Project.updateOne.lastCall.args[1].$addToSet
  175. ).to.have.keys('tokenAccessReadAndWrite_refs')
  176. sinon.assert.calledWith(
  177. this.Analytics.recordEventForUser,
  178. this.userId,
  179. 'project-joined',
  180. { mode: 'read-write' }
  181. )
  182. done()
  183. }
  184. )
  185. })
  186. it('should not produce an error', function (done) {
  187. this.TokenAccessHandler.addReadAndWriteUserToProject(
  188. this.userId,
  189. this.projectId,
  190. err => {
  191. expect(err).to.not.exist
  192. done()
  193. }
  194. )
  195. })
  196. describe('when Project.updateOne produces an error', function () {
  197. beforeEach(function () {
  198. this.Project.updateOne = sinon
  199. .stub()
  200. .callsArgWith(2, new Error('woops'))
  201. })
  202. it('should produce an error', function (done) {
  203. this.TokenAccessHandler.addReadAndWriteUserToProject(
  204. this.userId,
  205. this.projectId,
  206. err => {
  207. expect(err).to.exist
  208. done()
  209. }
  210. )
  211. })
  212. })
  213. })
  214. describe('grantSessionTokenAccess', function () {
  215. beforeEach(function () {
  216. this.req = { session: {}, headers: {} }
  217. })
  218. it('should add the token to the session', function (done) {
  219. this.TokenAccessHandler.grantSessionTokenAccess(
  220. this.req,
  221. this.projectId,
  222. this.token
  223. )
  224. expect(
  225. this.req.session.anonTokenAccess[this.projectId.toString()]
  226. ).to.equal(this.token)
  227. done()
  228. })
  229. })
  230. describe('validateTokenForAnonymousAccess', function () {
  231. describe('when a read-only project is found', function () {
  232. beforeEach(function () {
  233. this.TokenAccessHandler.getTokenType = sinon.stub().returns('readOnly')
  234. this.TokenAccessHandler.getProjectByToken = sinon
  235. .stub()
  236. .callsArgWith(2, null, this.project)
  237. })
  238. it('should try to find projects with both kinds of token', function (done) {
  239. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  240. this.projectId,
  241. this.token,
  242. (err, allowed) => {
  243. expect(err).to.not.exist
  244. expect(
  245. this.TokenAccessHandler.getProjectByToken.callCount
  246. ).to.equal(1)
  247. done()
  248. }
  249. )
  250. })
  251. it('should allow read-only access', function (done) {
  252. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  253. this.projectId,
  254. this.token,
  255. (err, rw, ro) => {
  256. expect(err).to.not.exist
  257. expect(rw).to.equal(false)
  258. expect(ro).to.equal(true)
  259. done()
  260. }
  261. )
  262. })
  263. })
  264. describe('when a read-and-write project is found', function () {
  265. beforeEach(function () {
  266. this.TokenAccessHandler.getTokenType = sinon
  267. .stub()
  268. .returns('readAndWrite')
  269. this.TokenAccessHandler.getProjectByToken = sinon
  270. .stub()
  271. .callsArgWith(2, null, this.project)
  272. })
  273. describe('when Anonymous token access is not enabled', function (done) {
  274. beforeEach(function () {
  275. this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
  276. })
  277. it('should try to find projects with both kinds of token', function (done) {
  278. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  279. this.projectId,
  280. this.token,
  281. (err, rw, ro) => {
  282. expect(err).to.not.exist
  283. expect(
  284. this.TokenAccessHandler.getProjectByToken.callCount
  285. ).to.equal(1)
  286. done()
  287. }
  288. )
  289. })
  290. it('should not allow read-and-write access', function (done) {
  291. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  292. this.projectId,
  293. this.token,
  294. (err, rw, ro) => {
  295. expect(err).to.not.exist
  296. expect(rw).to.equal(false)
  297. expect(ro).to.equal(false)
  298. done()
  299. }
  300. )
  301. })
  302. })
  303. describe('when anonymous token access is enabled', function (done) {
  304. beforeEach(function () {
  305. this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true
  306. })
  307. it('should try to find projects with both kinds of token', function (done) {
  308. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  309. this.projectId,
  310. this.token,
  311. (err, rw, ro) => {
  312. expect(err).to.not.exist
  313. expect(
  314. this.TokenAccessHandler.getProjectByToken.callCount
  315. ).to.equal(1)
  316. done()
  317. }
  318. )
  319. })
  320. it('should allow read-and-write access', function (done) {
  321. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  322. this.projectId,
  323. this.token,
  324. (err, rw, ro) => {
  325. expect(err).to.not.exist
  326. expect(rw).to.equal(true)
  327. expect(ro).to.equal(false)
  328. done()
  329. }
  330. )
  331. })
  332. })
  333. })
  334. describe('when no project is found', function () {
  335. beforeEach(function () {
  336. this.TokenAccessHandler.getProjectByToken = sinon
  337. .stub()
  338. .callsArgWith(2, null, null, null)
  339. })
  340. it('should try to find projects with both kinds of token', function (done) {
  341. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  342. this.projectId,
  343. this.token,
  344. (err, allowed) => {
  345. expect(err).to.not.exist
  346. expect(
  347. this.TokenAccessHandler.getProjectByToken.callCount
  348. ).to.equal(1)
  349. done()
  350. }
  351. )
  352. })
  353. it('should not allow any access', function (done) {
  354. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  355. this.projectId,
  356. this.token,
  357. (err, rw, ro) => {
  358. expect(err).to.not.exist
  359. expect(rw).to.equal(false)
  360. expect(ro).to.equal(false)
  361. done()
  362. }
  363. )
  364. })
  365. })
  366. describe('when findProject produces an error', function () {
  367. beforeEach(function () {
  368. this.TokenAccessHandler.getProjectByToken = sinon
  369. .stub()
  370. .callsArgWith(2, new Error('woops'))
  371. })
  372. it('should try to find projects with both kinds of token', function (done) {
  373. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  374. this.projectId,
  375. this.token,
  376. (err, allowed) => {
  377. expect(err).to.exist
  378. expect(allowed).to.not.exist
  379. expect(
  380. this.TokenAccessHandler.getProjectByToken.callCount
  381. ).to.equal(1)
  382. done()
  383. }
  384. )
  385. })
  386. it('should produce an error and not allow access', function (done) {
  387. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  388. this.projectId,
  389. this.token,
  390. (err, rw, ro) => {
  391. expect(err).to.exist
  392. expect(err).to.be.instanceof(Error)
  393. expect(rw).to.equal(undefined)
  394. expect(ro).to.equal(undefined)
  395. done()
  396. }
  397. )
  398. })
  399. })
  400. describe('when project is not set to token-based access', function () {
  401. beforeEach(function () {
  402. this.project.publicAccesLevel = 'private'
  403. })
  404. describe('for read-and-write project', function () {
  405. beforeEach(function () {
  406. this.TokenAccessHandler.getTokenType = sinon
  407. .stub()
  408. .returns('readAndWrite')
  409. this.TokenAccessHandler.getProjectByToken = sinon
  410. .stub()
  411. .callsArgWith(2, null, this.project)
  412. })
  413. it('should not allow any access', function (done) {
  414. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  415. this.projectId,
  416. this.token,
  417. (err, rw, ro) => {
  418. expect(err).to.not.exist
  419. expect(rw).to.equal(false)
  420. expect(ro).to.equal(false)
  421. done()
  422. }
  423. )
  424. })
  425. })
  426. describe('for read-only project', function () {
  427. beforeEach(function () {
  428. this.TokenAccessHandler.getTokenType = sinon
  429. .stub()
  430. .returns('readOnly')
  431. this.TokenAccessHandler.getProjectByToken = sinon
  432. .stub()
  433. .callsArgWith(2, null, this.project)
  434. })
  435. it('should not allow any access', function (done) {
  436. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  437. this.projectId,
  438. this.token,
  439. (err, rw, ro) => {
  440. expect(err).to.not.exist
  441. expect(rw).to.equal(false)
  442. expect(ro).to.equal(false)
  443. done()
  444. }
  445. )
  446. })
  447. })
  448. describe('with nothing', function () {
  449. beforeEach(function () {
  450. this.TokenAccessHandler.getProjectByToken = sinon
  451. .stub()
  452. .callsArgWith(1, null, null, null)
  453. })
  454. it('should not allow any access', function (done) {
  455. this.TokenAccessHandler.validateTokenForAnonymousAccess(
  456. this.projectId,
  457. null,
  458. (err, rw, ro) => {
  459. expect(err).to.not.exist
  460. expect(rw).to.equal(false)
  461. expect(ro).to.equal(false)
  462. done()
  463. }
  464. )
  465. })
  466. })
  467. })
  468. })
  469. describe('getDocPublishedInfo', function () {
  470. beforeEach(function () {
  471. this.callback = sinon.stub()
  472. })
  473. describe('when v1 api not set', function () {
  474. beforeEach(function () {
  475. this.settings.apis = { v1: undefined }
  476. this.TokenAccessHandler.getV1DocPublishedInfo(this.token, this.callback)
  477. })
  478. it('should not check access and return default info', function () {
  479. expect(this.V1Api.request.called).to.equal(false)
  480. expect(
  481. this.callback.calledWith(null, {
  482. allow: true,
  483. })
  484. ).to.equal(true)
  485. })
  486. })
  487. describe('when v1 api is set', function () {
  488. beforeEach(function () {
  489. this.settings.apis = { v1: { url: 'v1Url' } }
  490. })
  491. describe('on V1Api.request success', function () {
  492. beforeEach(function () {
  493. this.V1Api.request = sinon
  494. .stub()
  495. .callsArgWith(1, null, null, 'mock-data')
  496. this.TokenAccessHandler.getV1DocPublishedInfo(
  497. this.token,
  498. this.callback
  499. )
  500. })
  501. it('should return response body', function () {
  502. expect(
  503. this.V1Api.request.calledWith({
  504. url: `/api/v1/overleaf/docs/${this.token}/is_published`,
  505. })
  506. ).to.equal(true)
  507. expect(this.callback.calledWith(null, 'mock-data')).to.equal(true)
  508. })
  509. })
  510. describe('on V1Api.request error', function () {
  511. beforeEach(function () {
  512. this.V1Api.request = sinon.stub().callsArgWith(1, 'error')
  513. this.TokenAccessHandler.getV1DocPublishedInfo(
  514. this.token,
  515. this.callback
  516. )
  517. })
  518. it('should callback with error', function () {
  519. expect(this.callback.calledWith('error')).to.equal(true)
  520. })
  521. })
  522. })
  523. })
  524. describe('getV1DocInfo', function () {
  525. beforeEach(function () {
  526. this.callback = sinon.stub()
  527. })
  528. describe('when v1 api not set', function () {
  529. beforeEach(function () {
  530. this.TokenAccessHandler.getV1DocInfo(
  531. this.token,
  532. this.v2UserId,
  533. this.callback
  534. )
  535. })
  536. it('should not check access and return default info', function () {
  537. expect(this.V1Api.request.called).to.equal(false)
  538. expect(
  539. this.callback.calledWith(null, {
  540. exists: true,
  541. exported: false,
  542. })
  543. ).to.equal(true)
  544. })
  545. })
  546. describe('when v1 api is set', function () {
  547. beforeEach(function () {
  548. this.settings.apis = { v1: 'v1' }
  549. })
  550. describe('on V1Api.request success', function () {
  551. beforeEach(function () {
  552. this.V1Api.request = sinon
  553. .stub()
  554. .callsArgWith(1, null, null, 'mock-data')
  555. this.TokenAccessHandler.getV1DocInfo(
  556. this.token,
  557. this.v2UserId,
  558. this.callback
  559. )
  560. })
  561. it('should return response body', function () {
  562. expect(
  563. this.V1Api.request.calledWith({
  564. url: `/api/v1/overleaf/docs/${this.token}/info`,
  565. })
  566. ).to.equal(true)
  567. expect(this.callback.calledWith(null, 'mock-data')).to.equal(true)
  568. })
  569. })
  570. describe('on V1Api.request error', function () {
  571. beforeEach(function () {
  572. this.V1Api.request = sinon.stub().callsArgWith(1, 'error')
  573. this.TokenAccessHandler.getV1DocInfo(
  574. this.token,
  575. this.v2UserId,
  576. this.callback
  577. )
  578. })
  579. it('should callback with error', function () {
  580. expect(this.callback.calledWith('error')).to.equal(true)
  581. })
  582. })
  583. })
  584. })
  585. describe('createTokenHashPrefix', function () {
  586. it('creates a prefix of the hash', function () {
  587. const prefix =
  588. this.TokenAccessHandler.createTokenHashPrefix('zxpxjrwdtsgd')
  589. expect(prefix.length).to.equal(6)
  590. })
  591. })
  592. describe('normalizeTokenHashPrefix', function () {
  593. const cases = {
  594. // hex string
  595. ab2345: 'ab2345',
  596. '01234f': '01234f',
  597. '012345': '012345',
  598. // remove (encoded) hash
  599. '#012345': '012345',
  600. '%23012345': '012345',
  601. // remove trailing special characters
  602. '012345.': '012345',
  603. '012345/': '012345',
  604. // v1 doc
  605. '%2F1234567%2F': '%2F1234567%2F',
  606. }
  607. for (const [input, output] of Object.entries(cases)) {
  608. it(`should handle ${JSON.stringify(input)}`, function () {
  609. expect(
  610. this.TokenAccessHandler.normalizeTokenHashPrefix(input)
  611. ).to.equal(output)
  612. })
  613. }
  614. })
  615. describe('checkTokenHashPrefix', function () {
  616. const userId = 'abc123'
  617. const projectId = 'def456'
  618. it('sends "match" to metrics when prefix matches the prefix of the hash of the token', function () {
  619. const token = 'zxpxjrwdtsgd'
  620. const prefix = this.TokenAccessHandler.createTokenHashPrefix(token)
  621. this.TokenAccessHandler.checkTokenHashPrefix(
  622. token,
  623. `#${prefix}`,
  624. 'readOnly',
  625. userId,
  626. { projectId }
  627. )
  628. expect(this.Metrics.inc).to.have.been.calledWith(
  629. 'link-sharing.hash-check',
  630. {
  631. path: 'readOnly',
  632. status: 'match',
  633. }
  634. )
  635. })
  636. it('sends "mismatch" to metrics when prefix does not match the prefix of the hash of the token', function () {
  637. const token = 'zxpxjrwdtsgd'
  638. const prefix = this.TokenAccessHandler.createTokenHashPrefix(token)
  639. this.TokenAccessHandler.checkTokenHashPrefix(
  640. 'anothertoken',
  641. `#${prefix}`,
  642. 'readOnly',
  643. userId,
  644. { projectId }
  645. )
  646. expect(this.Metrics.inc).to.have.been.calledWith(
  647. 'link-sharing.hash-check',
  648. {
  649. path: 'readOnly',
  650. status: 'mismatch',
  651. }
  652. )
  653. expect(this.logger.info).to.have.been.calledWith(
  654. {
  655. tokenHashPrefix: prefix,
  656. hashPrefixStatus: 'mismatch',
  657. userId,
  658. projectId,
  659. type: 'readOnly',
  660. },
  661. 'mismatched token hash prefix'
  662. )
  663. })
  664. it('sends "missing" to metrics when prefix is undefined', function () {
  665. this.TokenAccessHandler.checkTokenHashPrefix(
  666. 'anothertoken',
  667. undefined,
  668. 'readOnly',
  669. userId,
  670. { projectId }
  671. )
  672. expect(this.Metrics.inc).to.have.been.calledWith(
  673. 'link-sharing.hash-check',
  674. {
  675. path: 'readOnly',
  676. status: 'missing',
  677. }
  678. )
  679. })
  680. it('sends "missing" to metrics when URL hash is sent as "#" only', function () {
  681. this.TokenAccessHandler.checkTokenHashPrefix(
  682. 'anothertoken',
  683. '#',
  684. 'readOnly',
  685. userId,
  686. { projectId }
  687. )
  688. expect(this.Metrics.inc).to.have.been.calledWith(
  689. 'link-sharing.hash-check',
  690. {
  691. path: 'readOnly',
  692. status: 'missing',
  693. }
  694. )
  695. })
  696. it('handles encoded hashtags', function () {
  697. const token = 'zxpxjrwdtsgd'
  698. const prefix = this.TokenAccessHandler.createTokenHashPrefix(token)
  699. this.TokenAccessHandler.checkTokenHashPrefix(
  700. token,
  701. `%23${prefix}`,
  702. 'readOnly',
  703. userId,
  704. { projectId }
  705. )
  706. expect(this.Metrics.inc).to.have.been.calledWith(
  707. 'link-sharing.hash-check',
  708. {
  709. path: 'readOnly',
  710. status: 'match',
  711. }
  712. )
  713. })
  714. it('sends "mismatch-v1-format" for suspected v1 URLs with 7 numbers in URL fragment', function () {
  715. const token = '4112142489ddsbkrdzhxrq'
  716. const prefix = '%2F1234567%2F'
  717. this.TokenAccessHandler.checkTokenHashPrefix(
  718. token,
  719. `#${prefix}`,
  720. 'readAndWrite',
  721. userId,
  722. { projectId }
  723. )
  724. expect(this.Metrics.inc).to.have.been.calledWith(
  725. 'link-sharing.hash-check',
  726. {
  727. path: 'readAndWrite',
  728. status: 'mismatch-v1-format',
  729. }
  730. )
  731. })
  732. it('sends "mismatch-v1-format" for suspected v1 URLs with 8 numbers in URL fragment', function () {
  733. const token = '4112142489ddsbkrdzhxrq'
  734. const prefix = '%2F12345678%2F'
  735. this.TokenAccessHandler.checkTokenHashPrefix(
  736. token,
  737. `#${prefix}`,
  738. 'readAndWrite',
  739. userId,
  740. { projectId }
  741. )
  742. expect(this.Metrics.inc).to.have.been.calledWith(
  743. 'link-sharing.hash-check',
  744. {
  745. path: 'readAndWrite',
  746. status: 'mismatch-v1-format',
  747. }
  748. )
  749. })
  750. })
  751. })