TokenAccessHandlerTests.js 23 KB

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