@openneuro/server 4.23.0 → 4.24.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Dockerfile CHANGED
@@ -4,7 +4,7 @@ FROM openneuro/node AS build
4
4
  WORKDIR /srv/packages/openneuro-server
5
5
  RUN yarn build
6
6
 
7
- FROM node:18.17.1-alpine
7
+ FROM node:20.12.2-alpine
8
8
 
9
9
  WORKDIR /srv
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/server",
3
- "version": "4.23.0",
3
+ "version": "4.24.0-alpha.2",
4
4
  "description": "Core service for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "src/server.js",
@@ -18,10 +18,10 @@
18
18
  "@apollo/client": "3.7.2",
19
19
  "@apollo/server": "4.9.3",
20
20
  "@apollo/utils.keyvadapter": "3.0.0",
21
- "@elastic/elasticsearch": "7.15.0",
21
+ "@elastic/elasticsearch": "8.13.1",
22
22
  "@graphql-tools/schema": "^10.0.0",
23
23
  "@keyv/redis": "^2.7.0",
24
- "@openneuro/search": "^4.23.0",
24
+ "@openneuro/search": "^4.24.0-alpha.2",
25
25
  "@passport-next/passport-google-oauth2": "^1.0.0",
26
26
  "@sentry/node": "^4.5.3",
27
27
  "base64url": "^3.0.0",
@@ -30,8 +30,8 @@
30
30
  "date-fns": "^2.16.1",
31
31
  "draft-js": "^0.11.7",
32
32
  "draft-js-export-html": "^1.4.1",
33
- "elastic-apm-node": "^4.3.0",
34
- "express": "4.18.2",
33
+ "elastic-apm-node": "4.5.4",
34
+ "express": "4.19.2",
35
35
  "graphql": "16.8.1",
36
36
  "graphql-bigint": "^1.0.0",
37
37
  "graphql-compose": "9.0.10",
@@ -39,11 +39,12 @@
39
39
  "graphql-tools": "9.0.0",
40
40
  "immutable": "^3.8.2",
41
41
  "ioredis": "4.17.3",
42
- "jsdom": "^11.6.2",
42
+ "jsdom": "24.0.0",
43
43
  "jsonwebtoken": "^9.0.0",
44
44
  "keyv": "^4.5.3",
45
45
  "mime-types": "^2.1.19",
46
- "mongoose": "^6.11.3",
46
+ "mongodb-memory-server": "^9.2.0",
47
+ "mongoose": "6.12.8",
47
48
  "morgan": "^1.6.1",
48
49
  "node-mailjet": "^3.3.5",
49
50
  "object-hash": "2.1.1",
@@ -76,14 +77,14 @@
76
77
  "@types/semver": "^5",
77
78
  "core-js": "^3.10.1",
78
79
  "ioredis-mock": "^8.8.1",
79
- "nodemon": "^2.0.7",
80
+ "nodemon": "3.1.0",
80
81
  "ts-node-dev": "1.1.6",
81
82
  "tsc-watch": "^4.2.9",
82
- "vitest": "0.34.4",
83
+ "vitest": "1.5.0",
83
84
  "vitest-fetch-mock": "0.2.2"
84
85
  },
85
86
  "publishConfig": {
86
87
  "access": "public"
87
88
  },
88
- "gitHead": "e86df78635d7ac6cfa03a2add5760b8539103583"
89
+ "gitHead": "aa9717595d41e5e0eb55328c62fa59c80847b95e"
89
90
  }
@@ -12,4 +12,5 @@ export enum CacheType {
12
12
  snapshotIndex = "snapshotIndex",
13
13
  participantCount = "participantCount",
14
14
  snapshotDownload = "download",
15
+ draftRevision = "revision",
15
16
  }
@@ -3,6 +3,7 @@ import request from "superagent"
3
3
  import { createDataset, datasetsFilter, testBlacklist } from "../dataset"
4
4
  import { getDatasetWorker } from "../../libs/datalad-service"
5
5
  import { connect } from "mongoose"
6
+ import { MongoMemoryServer } from "mongodb-memory-server"
6
7
 
7
8
  // Mock requests to Datalad service
8
9
  vi.mock("superagent")
@@ -13,9 +14,11 @@ vi.mock("../../libs/notifications")
13
14
 
14
15
  describe("dataset model operations", () => {
15
16
  describe("createDataset()", () => {
16
- beforeAll(() => {
17
+ let mongod
18
+ beforeAll(async () => {
17
19
  // Setup MongoDB with mongodb-memory-server
18
- connect(globalThis.__MONGO_URI__)
20
+ mongod = await MongoMemoryServer.create()
21
+ connect(mongod.getUri())
19
22
  })
20
23
  it("resolves to dataset id string", async () => {
21
24
  const user = { id: "1234" }
@@ -1,5 +1,6 @@
1
1
  import { vi } from "vitest"
2
2
  vi.mock("ioredis")
3
+ import { MongoMemoryServer } from "mongodb-memory-server"
3
4
  import * as pagination from "../pagination.js"
4
5
  import { connect, Types } from "mongoose"
5
6
  import Dataset from "../../models/dataset"
@@ -37,8 +38,11 @@ describe("pagination model operations", () => {
37
38
  })
38
39
  })
39
40
  describe("datasetsConnection()", () => {
41
+ let mongod
40
42
  beforeAll(async () => {
41
- await connect(globalThis.__MONGO_URI__)
43
+ // Setup MongoDB with mongodb-memory-server
44
+ mongod = await MongoMemoryServer.create()
45
+ connect(mongod.getUri())
42
46
  const ds = new Dataset({
43
47
  _id: new ObjectID("5bef51a1ed211400c08e5524"),
44
48
  id: "ds001001",
@@ -1,5 +1,6 @@
1
1
  import { vi } from "vitest"
2
2
  vi.mock("ioredis")
3
+ import { MongoMemoryServer } from "mongodb-memory-server"
3
4
  import request from "superagent"
4
5
  import { createDataset } from "../dataset"
5
6
  import { createSnapshot } from "../snapshots"
@@ -25,10 +26,15 @@ vi.mock("../../libs/notifications.ts")
25
26
 
26
27
  describe("snapshot model operations", () => {
27
28
  describe("createSnapshot()", () => {
29
+ let mongod
30
+ beforeAll(async () => {
31
+ // Setup MongoDB with mongodb-memory-server
32
+ mongod = await MongoMemoryServer.create()
33
+ connect(mongod.getUri())
34
+ })
28
35
  it("posts to the DataLad /datasets/{dsId}/snapshots/{snapshot} endpoint", async () => {
29
36
  const user = { id: "1234" }
30
37
  const tag = "snapshot"
31
- await connect(globalThis.__MONGO_URI__)
32
38
  const { id: dsId } = await createDataset(user.id, user, {
33
39
  affirmedDefaced: true,
34
40
  affirmedConsent: true,
@@ -88,6 +88,12 @@ export const repairDescriptionTypes = (description) => {
88
88
  newDescription.HowToAcknowledge =
89
89
  JSON.stringify(description.HowToAcknowledge) || ""
90
90
  }
91
+ if (
92
+ description.hasOwnProperty("DatasetType") &&
93
+ typeof description.DatasetType !== "string"
94
+ ) {
95
+ newDescription.DatasetType = "raw"
96
+ }
91
97
  return newDescription
92
98
  }
93
99
 
@@ -4,16 +4,21 @@
4
4
  import request from "superagent"
5
5
  import Dataset from "../models/dataset"
6
6
  import { getDatasetWorker } from "../libs/datalad-service"
7
+ import CacheItem, { CacheType } from "../cache/item"
8
+ import { redis } from "../libs/redis"
7
9
 
8
10
  export const getDraftRevision = async (datasetId) => {
9
- const draftUrl = `http://${
10
- getDatasetWorker(
11
- datasetId,
12
- )
13
- }/datasets/${datasetId}/draft`
14
- const response = await fetch(draftUrl)
15
- const { hexsha } = await response.json()
16
- return hexsha
11
+ const cache = new CacheItem(redis, CacheType.draftRevision, [datasetId], 10)
12
+ return cache.get(async (_doNotCache): Promise<string> => {
13
+ const draftUrl = `http://${
14
+ getDatasetWorker(
15
+ datasetId,
16
+ )
17
+ }/datasets/${datasetId}/draft`
18
+ const response = await fetch(draftUrl)
19
+ const { hexsha } = await response.json()
20
+ return hexsha
21
+ })
17
22
  }
18
23
 
19
24
  export const updateDatasetRevision = (datasetId, gitRef) => {
@@ -1,5 +1,5 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`resolver permissions helpers > checkDatasetAdmin() > resolves to false for anonymous users 1`] = `"You do not have admin access to this dataset."`;
3
+ exports[`resolver permissions helpers > checkDatasetAdmin() > resolves to false for anonymous users 1`] = `[Error: You do not have admin access to this dataset.]`;
4
4
 
5
- exports[`resolver permissions helpers > checkDatasetWrite() > resolves to false for anonymous users 1`] = `"You do not have access to modify this dataset."`;
5
+ exports[`resolver permissions helpers > checkDatasetWrite() > resolves to false for anonymous users 1`] = `[Error: You do not have access to modify this dataset.]`;
@@ -1,6 +1,7 @@
1
1
  import { vi } from "vitest"
2
2
  import { connect } from "mongoose"
3
3
  import { deleteComment, flatten } from "../resolvers/comment"
4
+ import { MongoMemoryServer } from "mongodb-memory-server"
4
5
  import Comment from "../../models/comment"
5
6
 
6
7
  vi.mock("ioredis")
@@ -16,8 +17,11 @@ describe("comment resolver helpers", () => {
16
17
  user: "5678",
17
18
  userInfo: { admin: false },
18
19
  }
20
+ let mongod
19
21
  beforeAll(async () => {
20
- await connect(globalThis.__MONGO_URI__)
22
+ // Setup MongoDB with mongodb-memory-server
23
+ mongod = await MongoMemoryServer.create()
24
+ connect(mongod.getUri())
21
25
  const comment = new Comment({
22
26
  text: "a",
23
27
  createDate: new Date().toISOString(),
@@ -28,11 +28,9 @@ describe("dataset search resolvers", () => {
28
28
  describe("elasticRelayConnection()", () => {
29
29
  it("returns a relay cursor for empty ApiResponse", async () => {
30
30
  const emptyApiResponse = {
31
- body: {
32
- hits: {
33
- hits: [],
34
- total: { value: 0 },
35
- },
31
+ hits: {
32
+ hits: [],
33
+ total: { value: 0 },
36
34
  },
37
35
  }
38
36
 
@@ -60,20 +58,18 @@ describe("dataset search resolvers", () => {
60
58
  }
61
59
 
62
60
  const expectedApiResponse = {
63
- body: {
64
- hits: {
65
- hits: [
66
- { _source: { id: "testdataset1" } },
67
- { _source: { id: "testdataset2" } },
68
- { _source: { id: "testdataset3" }, sort: [1] },
69
- ],
70
- total: { value: 10 },
71
- },
61
+ hits: {
62
+ hits: [
63
+ { _source: { id: "testdataset1" } },
64
+ { _source: { id: "testdataset2" } },
65
+ { _source: { id: "testdataset3" }, sort: [1] },
66
+ ],
67
+ total: { value: 10 },
72
68
  },
73
69
  }
74
70
 
75
71
  const resultsRelayConnection = {
76
- edges: expectedApiResponse.body.hits.hits.map((hit) => {
72
+ edges: expectedApiResponse.hits.hits.map((hit) => {
77
73
  // This skips the dataset resolver logic and passes this back
78
74
  mockResolvers.dataset.mockReturnValueOnce(hit._source)
79
75
  return {
@@ -1,4 +1,5 @@
1
1
  import { vi } from "vitest"
2
+ import { MongoMemoryServer } from "mongodb-memory-server"
2
3
  import { connect } from "mongoose"
3
4
  import request from "superagent"
4
5
  import * as ds from "../dataset"
@@ -9,8 +10,11 @@ vi.mock("../../../config.ts")
9
10
  vi.mock("../../../libs/notifications.ts")
10
11
 
11
12
  describe("dataset resolvers", () => {
12
- beforeAll(() => {
13
- connect(globalThis.__MONGO_URI__)
13
+ let mongod
14
+ beforeAll(async () => {
15
+ // Setup MongoDB with mongodb-memory-server
16
+ mongod = await MongoMemoryServer.create()
17
+ connect(mongod.getUri())
14
18
  })
15
19
  describe("createDataset()", () => {
16
20
  it("createDataset mutation succeeds", async () => {
@@ -4,6 +4,7 @@ import Star from "../../models/stars"
4
4
  import Subscription from "../../models/subscription"
5
5
  import Permission from "../../models/permission"
6
6
  import { hashObject } from "../../libs/authentication/crypto"
7
+ import util from "util"
7
8
 
8
9
  const elasticIndex = "datasets"
9
10
 
@@ -37,7 +38,7 @@ export const decodeCursor = (cursor) =>
37
38
  * @param {import ('@elastic/elasticsearch').ApiResponse} result
38
39
  */
39
40
  export const elasticRelayConnection = (
40
- { body },
41
+ body,
41
42
  id,
42
43
  size,
43
44
  childResolvers = { dataset },
@@ -102,9 +103,9 @@ export const datasetSearchConnection = async (
102
103
  index: elasticIndex,
103
104
  size: first,
104
105
  q: `${q} AND public:true`,
105
- body: requestBody,
106
+ ...requestBody,
106
107
  })
107
- return elasticRelayConnection(result, searchId, first)
108
+ return elasticRelayConnection(requestBody, searchId, first)
108
109
  }
109
110
 
110
111
  export const datasetSearch = {
@@ -215,6 +216,7 @@ export const advancedDatasetSearchConnection = async (
215
216
  },
216
217
  { user, userInfo },
217
218
  ) => {
219
+ // Create an identity for this search (used to cache connections)
218
220
  const searchId = hashObject({
219
221
  query,
220
222
  datasetType,
@@ -223,26 +225,30 @@ export const advancedDatasetSearchConnection = async (
223
225
  user,
224
226
  })
225
227
  const sort = [{ _score: "desc" }, { id: "desc" }]
226
- if (sortBy) sort.unshift(sortBy)
227
- const requestBody = {
228
- sort,
229
- query: allDatasets
230
- ? query
231
- : await parseQuery(query, datasetType, datasetStatus, user),
232
- search_after: undefined,
228
+ if (sortBy) {
229
+ sort.unshift(sortBy)
233
230
  }
231
+ // Parse out the decode token and add it to our query if successful
232
+ let search_after
234
233
  if (after) {
235
234
  try {
236
- requestBody.search_after = decodeCursor(after)
237
- } catch (err) {
235
+ search_after = decodeCursor(after)
236
+ } catch (_err) {
238
237
  // Don't include search_after if parsing fails
239
238
  }
240
239
  }
241
- const result = await elasticClient.search({
240
+ const requestBody = {
242
241
  index: elasticIndex,
243
242
  size: first,
244
- body: requestBody,
245
- })
243
+ sort,
244
+ query: allDatasets
245
+ ? query
246
+ : await parseQuery(query, datasetType, datasetStatus, user),
247
+ search_after,
248
+ }
249
+ // Run the query
250
+ const result = await elasticClient.search(requestBody)
251
+ // Extend with relay connection pagination
246
252
  return elasticRelayConnection(
247
253
  result,
248
254
  searchId,
@@ -571,6 +571,8 @@ export const typeDefs = `
571
571
  ReferencesAndLinks: [String]
572
572
  # The Document Object Identifier of the dataset (not the corresponding paper).
573
573
  DatasetDOI: String
574
+ # The BIDS DatasetType field defined as "raw" or "derivative"
575
+ DatasetType: String
574
576
  # List of ethics committee approvals of the research protocols and/or protocol identifiers.
575
577
  EthicsApprovals: [String]
576
578
  }
@@ -3,6 +3,7 @@ import { Readable } from "node:stream"
3
3
  import mime from "mime-types"
4
4
  import { getFiles } from "../datalad/files"
5
5
  import { getDatasetWorker } from "../libs/datalad-service"
6
+ import { getDraftRevision } from "../datalad/draft"
6
7
 
7
8
  /**
8
9
  * Handlers for datalad dataset manipulation
@@ -21,7 +22,9 @@ export const getFile = async (req, res) => {
21
22
  const worker = getDatasetWorker(datasetId)
22
23
  // Find the right tree
23
24
  const pathComponents = filename.split(":")
24
- let tree = snapshotId || "HEAD"
25
+ // Get the draft commit for cache busting
26
+ const draftCommit = await getDraftRevision(datasetId)
27
+ let tree = snapshotId || draftCommit
25
28
  let file
26
29
  for (const level of pathComponents) {
27
30
  try {
@@ -1,13 +1,17 @@
1
1
  import { vi } from "vitest"
2
- import { connect } from "mongoose"
2
+ import { MongoMemoryServer } from "mongodb-memory-server"
3
+ import { connect, disconnect } from "mongoose"
3
4
  import { getAccessionNumber } from "../dataset"
4
5
 
5
6
  vi.mock("ioredis")
6
7
 
7
8
  describe("libs/dataset", () => {
8
9
  describe("getAccessionNumber", () => {
9
- beforeAll(() => {
10
- connect(globalThis.__MONGO_URI__)
10
+ let mongod
11
+ beforeAll(async () => {
12
+ // Setup MongoDB with mongodb-memory-server
13
+ mongod = await MongoMemoryServer.create()
14
+ await connect(mongod.getUri())
11
15
  })
12
16
  it('returns strings starting with "ds"', async () => {
13
17
  const ds = await getAccessionNumber()
@@ -1,17 +1,17 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`DOI minting utils > template() > accepts expected arguments 1`] = `
4
- "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
5
- <resource xmlns:xsi=\\"http://www.w3.org/2001/XMLSchema-instance\\" xmlns=\\"http://datacite.org/schema/kernel-4\\" xsi:schemaLocation=\\"http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd\\">
6
- <identifier identifierType=\\"DOI\\">12345</identifier>
4
+ "<?xml version="1.0" encoding="UTF-8"?>
5
+ <resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" xsi:schemaLocation="http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4/metadata.xsd">
6
+ <identifier identifierType="DOI">12345</identifier>
7
7
  <creators>
8
8
  <creator><creatorName>A. User</creatorName></creator><creator><creatorName>B. User</creatorName></creator>
9
9
  </creators>
10
10
  <titles>
11
- <title xml:lang=\\"en-us\\">Test Dataset</title>
11
+ <title xml:lang="en-us">Test Dataset</title>
12
12
  </titles>
13
13
  <publisher>Openneuro</publisher>
14
14
  <publicationYear>1999</publicationYear>
15
- <resourceType resourceTypeGeneral=\\"Dataset\\">fMRI</resourceType>
15
+ <resourceType resourceTypeGeneral="Dataset">fMRI</resourceType>
16
16
  </resource>"
17
17
  `;
@@ -56,22 +56,22 @@ exports[`email template -> comment created > renders with expected arguments 1`]
56
56
  </style>
57
57
  </head>
58
58
  <body>
59
- <div class=\\"top-bar\\">
60
- <img src=\\"https://openneuro.org/assets/email-header.1cb8bf76.png\\" />
59
+ <div class="top-bar">
60
+ <img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
61
61
  </div>
62
- <div class=\\"content\\">
62
+ <div class="content">
63
63
  <h2>Hi, J. Doe</h2>
64
64
 
65
65
  <p>
66
66
  A new new has been posted on a dataset you follow, <b>Not Real Dataset</b>.
67
67
  </p>
68
- <div class=\\"comment\\">
68
+ <div class="comment">
69
69
  <p>By: <b>56789</b> on 2063-04-05</p>
70
70
 
71
71
  Test comment, please ignore
72
72
  </div>
73
- <div class=\\"link-div\\">
74
- <a class=\\"dataset-link\\" href=\\"https://openneuro.org/datasets/ds1245678#comment-12345\\">Click here to view this comment on OpenNeuro &raquo;</a>
73
+ <div class="link-div">
74
+ <a class="dataset-link" href="https://openneuro.org/datasets/ds1245678#comment-12345">Click here to view this comment on OpenNeuro &raquo;</a>
75
75
  </div>
76
76
 
77
77
  <p>
@@ -82,7 +82,7 @@ exports[`email template -> comment created > renders with expected arguments 1`]
82
82
  </div>
83
83
  </body>
84
84
  <footer>
85
- If you would like to stop receiving notifications about this dataset, please <a class='link' href=\\"https://openneuro.org/datasets/ds1245678\\">visit the dataset page</a> and click the 'unfollow' icon.
85
+ If you would like to stop receiving notifications about this dataset, please <a class='link' href="https://openneuro.org/datasets/ds1245678">visit the dataset page</a> and click the 'unfollow' icon.
86
86
  </footer>
87
87
  <html>"
88
88
  `;
@@ -38,10 +38,10 @@ exports[`email template -> comment created > renders with expected arguments 1`]
38
38
  </style>
39
39
  </head>
40
40
  <body>
41
- <div class=\\"top-bar\\">
42
- <img src=\\"https://openneuro.org/assets/email-header.1cb8bf76.png\\" />
41
+ <div class="top-bar">
42
+ <img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
43
43
  </div>
44
- <div class=\\"content\\">
44
+ <div class="content">
45
45
  <h2>Hi, J. Doe</h2>
46
46
 
47
47
  <p>
@@ -38,10 +38,10 @@ exports[`email template -> comment created > renders with expected arguments 1`]
38
38
  </style>
39
39
  </head>
40
40
  <body>
41
- <div class=\\"top-bar\\">
42
- <img src=\\"https://openneuro.org/assets/email-header.1cb8bf76.png\\" />
41
+ <div class="top-bar">
42
+ <img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
43
43
  </div>
44
- <div class=\\"content\\">
44
+ <div class="content">
45
45
  <h2>Hi, J. Doe</h2>
46
46
 
47
47
  <p>
@@ -56,10 +56,10 @@ exports[`email template -> comment created > renders with expected arguments 1`]
56
56
  </style>
57
57
  </head>
58
58
  <body>
59
- <div class=\\"top-bar\\">
60
- <img src=\\"https://openneuro.org/assets/email-header.1cb8bf76.png\\" />
59
+ <div class="top-bar">
60
+ <img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
61
61
  </div>
62
- <div class=\\"content\\">
62
+ <div class="content">
63
63
  <h2>Hi, J. Doe</h2>
64
64
 
65
65
  <p>
@@ -67,12 +67,12 @@ exports[`email template -> comment created > renders with expected arguments 1`]
67
67
  </p>
68
68
 
69
69
  <div>
70
- <a class='dataset-link' href=\\"https://openneuro.org/datasets/ds1245678/versions/1.2.4\\">Click here to view this snapshot on OpenNeuro &raquo;</a>
70
+ <a class='dataset-link' href="https://openneuro.org/datasets/ds1245678/versions/1.2.4">Click here to view this snapshot on OpenNeuro &raquo;</a>
71
71
  </div>
72
72
 
73
73
  <div>
74
74
  <h4>Changelog</h4>
75
- <p class=\\"changelog\\">New changes...</p>
75
+ <p class="changelog">New changes...</p>
76
76
  </div>
77
77
 
78
78
  <p>
@@ -82,7 +82,7 @@ exports[`email template -> comment created > renders with expected arguments 1`]
82
82
  </div>
83
83
  </body>
84
84
  <footer>
85
- If you would like to stop receiving notifications about this dataset, please <a class='link' href=\\"https://openneuro.org/datasets/ds1245678\\">visit the dataset page</a> and click the 'unfollow' icon.
85
+ If you would like to stop receiving notifications about this dataset, please <a class='link' href="https://openneuro.org/datasets/ds1245678">visit the dataset page</a> and click the 'unfollow' icon.
86
86
  </footer>
87
87
  <html>"
88
88
  `;
@@ -38,10 +38,10 @@ exports[`email template -> comment created > renders with expected arguments 1`]
38
38
  </style>
39
39
  </head>
40
40
  <body>
41
- <div class=\\"top-bar\\">
42
- <img src=\\"https://openneuro.org/assets/email-header.1cb8bf76.png\\" />
41
+ <div class="top-bar">
42
+ <img src="https://openneuro.org/assets/email-header.1cb8bf76.png" />
43
43
  </div>
44
- <div class=\\"content\\">
44
+ <div class="content">
45
45
  <h2>Hi, J. Doe</h2>
46
46
 
47
47
  <p>
@@ -53,7 +53,7 @@ exports[`email template -> comment created > renders with expected arguments 1`]
53
53
  The CRN Team
54
54
  </p>
55
55
 
56
- <a class=\\"dataset-link\\" href=\\"https://openneuro.org/datasets/ds12345678/snapshot\\">Create a snapshot. &raquo;</a>
56
+ <a class="dataset-link" href="https://openneuro.org/datasets/ds12345678/snapshot">Create a snapshot. &raquo;</a>
57
57
  </div>
58
58
  </body>
59
59
  <html>"