|
|
@@ -2,6 +2,7 @@
|
|
|
|
|
|
const { expect } = require('chai')
|
|
|
const fs = require('node:fs')
|
|
|
+const { Readable } = require('node:stream')
|
|
|
const HTTPStatus = require('http-status')
|
|
|
const fetch = require('node-fetch')
|
|
|
const sinon = require('sinon')
|
|
|
@@ -15,6 +16,7 @@ const {
|
|
|
BlobStore,
|
|
|
persistChanges,
|
|
|
redisBuffer,
|
|
|
+ blobHash,
|
|
|
} = require('../../../../storage')
|
|
|
|
|
|
const { expectHttpError } = require('./support/expect_response')
|
|
|
@@ -145,29 +147,152 @@ describe('project controller', function () {
|
|
|
await populateProject(populatedMongoProjectId)
|
|
|
})
|
|
|
|
|
|
- it('handles empty postgres project', async function () {
|
|
|
- const { body } =
|
|
|
- await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
- body: { projectIds: [emptyPostgresProjectId] },
|
|
|
- })
|
|
|
- expect(body).to.deep.equal([
|
|
|
- {
|
|
|
- projectId: emptyPostgresProjectId,
|
|
|
+ describe('getProjectBlobsStats', function () {
|
|
|
+ it('handles empty postgres project', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
+ body: { projectIds: [emptyPostgresProjectId] },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal([
|
|
|
+ {
|
|
|
+ projectId: emptyPostgresProjectId,
|
|
|
+ textBlobBytes: 0,
|
|
|
+ binaryBlobBytes: 0,
|
|
|
+ totalBytes: 0,
|
|
|
+ nTextBlobs: 0,
|
|
|
+ nBinaryBlobs: 0,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ })
|
|
|
+ it('handles populated postgres project', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
+ body: { projectIds: [populatedPostgresProjectId] },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal([
|
|
|
+ {
|
|
|
+ projectId: populatedPostgresProjectId,
|
|
|
+ textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
+ binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
+ totalBytes:
|
|
|
+ testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
+ nTextBlobs: 1,
|
|
|
+ nBinaryBlobs: 1,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ })
|
|
|
+
|
|
|
+ it('handles empty mongo project', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
+ body: { projectIds: [emptyMongoProjectId] },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal([
|
|
|
+ {
|
|
|
+ projectId: emptyMongoProjectId,
|
|
|
+ textBlobBytes: 0,
|
|
|
+ binaryBlobBytes: 0,
|
|
|
+ totalBytes: 0,
|
|
|
+ nTextBlobs: 0,
|
|
|
+ nBinaryBlobs: 0,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ })
|
|
|
+ it('handles populated mongo project', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
+ body: { projectIds: [populatedMongoProjectId] },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal([
|
|
|
+ {
|
|
|
+ projectId: populatedMongoProjectId,
|
|
|
+ textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
+ binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
+ totalBytes:
|
|
|
+ testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
+ nTextBlobs: 1,
|
|
|
+ nBinaryBlobs: 1,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ })
|
|
|
+
|
|
|
+ it('handles batch of projects', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
+ body: {
|
|
|
+ projectIds: [
|
|
|
+ populatedPostgresProjectId,
|
|
|
+ populatedMongoProjectId,
|
|
|
+ emptyPostgresProjectId,
|
|
|
+ emptyMongoProjectId,
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal([
|
|
|
+ {
|
|
|
+ projectId: populatedPostgresProjectId,
|
|
|
+ textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
+ binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
+ totalBytes:
|
|
|
+ testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
+ nTextBlobs: 1,
|
|
|
+ nBinaryBlobs: 1,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ projectId: populatedMongoProjectId,
|
|
|
+ textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
+ binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
+ totalBytes:
|
|
|
+ testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
+ nTextBlobs: 1,
|
|
|
+ nBinaryBlobs: 1,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ projectId: emptyPostgresProjectId,
|
|
|
+ textBlobBytes: 0,
|
|
|
+ binaryBlobBytes: 0,
|
|
|
+ totalBytes: 0,
|
|
|
+ nTextBlobs: 0,
|
|
|
+ nBinaryBlobs: 0,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ projectId: emptyMongoProjectId,
|
|
|
+ textBlobBytes: 0,
|
|
|
+ binaryBlobBytes: 0,
|
|
|
+ totalBytes: 0,
|
|
|
+ nTextBlobs: 0,
|
|
|
+ nBinaryBlobs: 0,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ describe('getBlobStats', function () {
|
|
|
+ it('handles empty list of hashes', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getBlobStats({
|
|
|
+ project_id: populatedPostgresProjectId,
|
|
|
+ body: { blobHashes: [] },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal({
|
|
|
+ projectId: populatedPostgresProjectId,
|
|
|
textBlobBytes: 0,
|
|
|
binaryBlobBytes: 0,
|
|
|
totalBytes: 0,
|
|
|
nTextBlobs: 0,
|
|
|
nBinaryBlobs: 0,
|
|
|
- },
|
|
|
- ])
|
|
|
- })
|
|
|
- it('handles populated postgres project', async function () {
|
|
|
- const { body } =
|
|
|
- await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
- body: { projectIds: [populatedPostgresProjectId] },
|
|
|
})
|
|
|
- expect(body).to.deep.equal([
|
|
|
- {
|
|
|
+ })
|
|
|
+
|
|
|
+ it('handles a mix of text and binary blobs', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getBlobStats({
|
|
|
+ project_id: populatedPostgresProjectId,
|
|
|
+ body: {
|
|
|
+ blobHashes: [testFiles.HELLO_TXT_HASH, testFiles.GRAPH_PNG_HASH],
|
|
|
+ },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal({
|
|
|
projectId: populatedPostgresProjectId,
|
|
|
textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
@@ -175,92 +300,136 @@ describe('project controller', function () {
|
|
|
testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
nTextBlobs: 1,
|
|
|
nBinaryBlobs: 1,
|
|
|
- },
|
|
|
- ])
|
|
|
- })
|
|
|
-
|
|
|
- it('handles empty mongo project', async function () {
|
|
|
- const { body } =
|
|
|
- await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
- body: { projectIds: [emptyMongoProjectId] },
|
|
|
})
|
|
|
- expect(body).to.deep.equal([
|
|
|
- {
|
|
|
- projectId: emptyMongoProjectId,
|
|
|
- textBlobBytes: 0,
|
|
|
+ })
|
|
|
+
|
|
|
+ it('handles only text blobs', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getBlobStats({
|
|
|
+ project_id: populatedPostgresProjectId,
|
|
|
+ body: {
|
|
|
+ blobHashes: [testFiles.HELLO_TXT_HASH],
|
|
|
+ },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal({
|
|
|
+ projectId: populatedPostgresProjectId,
|
|
|
+ textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
binaryBlobBytes: 0,
|
|
|
- totalBytes: 0,
|
|
|
- nTextBlobs: 0,
|
|
|
+ totalBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
+ nTextBlobs: 1,
|
|
|
nBinaryBlobs: 0,
|
|
|
- },
|
|
|
- ])
|
|
|
- })
|
|
|
- it('handles populated mongo project', async function () {
|
|
|
- const { body } =
|
|
|
- await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
- body: { projectIds: [populatedMongoProjectId] },
|
|
|
})
|
|
|
- expect(body).to.deep.equal([
|
|
|
- {
|
|
|
- projectId: populatedMongoProjectId,
|
|
|
- textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
- binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
- totalBytes:
|
|
|
- testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
- nTextBlobs: 1,
|
|
|
- nBinaryBlobs: 1,
|
|
|
- },
|
|
|
- ])
|
|
|
- })
|
|
|
+ })
|
|
|
|
|
|
- it('handles batch of projects', async function () {
|
|
|
- const { body } =
|
|
|
- await testServer.basicAuthClient.apis.Project.getProjectBlobsStats({
|
|
|
- body: {
|
|
|
- projectIds: [
|
|
|
- populatedPostgresProjectId,
|
|
|
- populatedMongoProjectId,
|
|
|
- emptyPostgresProjectId,
|
|
|
- emptyMongoProjectId,
|
|
|
- ],
|
|
|
- },
|
|
|
- })
|
|
|
- expect(body).to.deep.equal([
|
|
|
- {
|
|
|
+ it('handles only binary blobs', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getBlobStats({
|
|
|
+ project_id: populatedPostgresProjectId,
|
|
|
+ body: {
|
|
|
+ blobHashes: [testFiles.GRAPH_PNG_HASH],
|
|
|
+ },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal({
|
|
|
projectId: populatedPostgresProjectId,
|
|
|
- textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
- binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
- totalBytes:
|
|
|
- testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
- nTextBlobs: 1,
|
|
|
- nBinaryBlobs: 1,
|
|
|
- },
|
|
|
- {
|
|
|
- projectId: populatedMongoProjectId,
|
|
|
- textBlobBytes: testFiles.HELLO_TXT_BYTE_LENGTH,
|
|
|
- binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
- totalBytes:
|
|
|
- testFiles.HELLO_TXT_BYTE_LENGTH + testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
- nTextBlobs: 1,
|
|
|
- nBinaryBlobs: 1,
|
|
|
- },
|
|
|
- {
|
|
|
- projectId: emptyPostgresProjectId,
|
|
|
textBlobBytes: 0,
|
|
|
- binaryBlobBytes: 0,
|
|
|
- totalBytes: 0,
|
|
|
+ binaryBlobBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
+ totalBytes: testFiles.GRAPH_PNG_BYTE_LENGTH,
|
|
|
nTextBlobs: 0,
|
|
|
- nBinaryBlobs: 0,
|
|
|
- },
|
|
|
- {
|
|
|
- projectId: emptyMongoProjectId,
|
|
|
+ nBinaryBlobs: 1,
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('handles non-existent blobs', async function () {
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getBlobStats({
|
|
|
+ project_id: populatedPostgresProjectId,
|
|
|
+ body: {
|
|
|
+ blobHashes: [testFiles.STRING_AB_HASH],
|
|
|
+ },
|
|
|
+ })
|
|
|
+ expect(body).to.deep.equal({
|
|
|
+ projectId: populatedPostgresProjectId,
|
|
|
textBlobBytes: 0,
|
|
|
binaryBlobBytes: 0,
|
|
|
totalBytes: 0,
|
|
|
nTextBlobs: 0,
|
|
|
nBinaryBlobs: 0,
|
|
|
- },
|
|
|
- ])
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it('throws an error for bad hashes', async function () {
|
|
|
+ await expectHttpError(
|
|
|
+ testServer.basicAuthClient.apis.Project.getBlobStats({
|
|
|
+ project_id: populatedPostgresProjectId,
|
|
|
+ body: {
|
|
|
+ blobHashes: ['non-existent-hash'],
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ HTTPStatus.INTERNAL_SERVER_ERROR
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it('handles a request with a large number of blobs', async function () {
|
|
|
+ const projectId = await testProjects.createEmptyProject()
|
|
|
+ const blobHashes = []
|
|
|
+ let expectedTextBytes = 0
|
|
|
+ let expectedBinaryBytes = 0
|
|
|
+ const nTextBlobs = 10
|
|
|
+ const nBinaryBlobs = 10
|
|
|
+
|
|
|
+ for (let i = 0; i < nTextBlobs; i++) {
|
|
|
+ const content = `text blob ${i}`
|
|
|
+ const hash = blobHash.fromString(content)
|
|
|
+ blobHashes.push(hash)
|
|
|
+ expectedTextBytes += content.length
|
|
|
+ const res = await fetch(
|
|
|
+ testServer.url(`/api/projects/${projectId}/blobs/${hash}`),
|
|
|
+ {
|
|
|
+ method: 'PUT',
|
|
|
+ body: content,
|
|
|
+ headers: { Authorization: testServer.basicAuthHeader },
|
|
|
+ }
|
|
|
+ )
|
|
|
+ expect(res.status).to.equal(HTTPStatus.CREATED)
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let i = 0; i < nBinaryBlobs; i++) {
|
|
|
+ const content = Buffer.from([0, i, i + 1, i + 2])
|
|
|
+ const hash = await blobHash.fromStream(
|
|
|
+ content.length,
|
|
|
+ Readable.from(content)
|
|
|
+ )
|
|
|
+ blobHashes.push(hash)
|
|
|
+ expectedBinaryBytes += content.length
|
|
|
+ const res = await fetch(
|
|
|
+ testServer.url(`/api/projects/${projectId}/blobs/${hash}`),
|
|
|
+ {
|
|
|
+ method: 'PUT',
|
|
|
+ body: content,
|
|
|
+ headers: {
|
|
|
+ Authorization: testServer.basicAuthHeader,
|
|
|
+ 'Content-Type': 'application/octet-stream',
|
|
|
+ },
|
|
|
+ }
|
|
|
+ )
|
|
|
+ expect(res.status).to.equal(HTTPStatus.CREATED)
|
|
|
+ }
|
|
|
+
|
|
|
+ const { body } =
|
|
|
+ await testServer.basicAuthClient.apis.Project.getBlobStats({
|
|
|
+ project_id: projectId,
|
|
|
+ body: { blobHashes },
|
|
|
+ })
|
|
|
+
|
|
|
+ expect(body).to.deep.equal({
|
|
|
+ projectId,
|
|
|
+ textBlobBytes: expectedTextBytes,
|
|
|
+ binaryBlobBytes: expectedBinaryBytes,
|
|
|
+ totalBytes: expectedTextBytes + expectedBinaryBytes,
|
|
|
+ nTextBlobs,
|
|
|
+ nBinaryBlobs,
|
|
|
+ })
|
|
|
+ })
|
|
|
})
|
|
|
})
|
|
|
|