From dd560884f6030818dd009cd13708f6659b1f4671 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:14:34 -0300 Subject: [PATCH 1/3] fix(missing dataContentType) - implements a class to validate the state of the file - skip file if any missing property is caught --- package.json | 2 +- src/arfs/arfs_builders/arfs_file_builders.ts | 34 ++++++----------- src/arfs/arfsdao.ts | 39 +++++++++++++------- src/arfs/arfsdao_anonymous.ts | 37 ++++++++++++++----- src/types/exceptions.ts | 36 ++++++++++++++++++ 5 files changed, 102 insertions(+), 46 deletions(-) create mode 100644 src/types/exceptions.ts diff --git a/package.json b/package.json index 3a791fa8..9c5813dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ardrive-core-js", - "version": "2.0.6", + "version": "2.0.7", "description": "ArDrive Core contains the essential back end application features to support the ArDrive CLI and Desktop apps, such as file management, Permaweb upload/download, wallet management and other common functions.", "main": "./lib/exports.js", "types": "./lib/exports.d.ts", diff --git a/src/arfs/arfs_builders/arfs_file_builders.ts b/src/arfs/arfs_builders/arfs_file_builders.ts index f1f0ecca..5a49dc31 100644 --- a/src/arfs/arfs_builders/arfs_file_builders.ts +++ b/src/arfs/arfs_builders/arfs_file_builders.ts @@ -18,7 +18,7 @@ import { Utf8ArrayToStr, extToMime } from '../../utils/common'; import { ArFSPublicFile, ArFSPrivateFile } from '../arfs_entities'; import { ArFSFileOrFolderBuilder } from './arfs_builders'; import { GatewayAPI } from '../../utils/gateway_api'; - +import { FileBuilderValidation, InvalidFileStateException } from '../../types/exceptions'; export interface FileMetaDataTransactionData extends EntityMetaDataTransactionData { // FIXME: do we need our safe types here? This interface refers to a JSON with primitive types name: string; @@ -86,16 +86,10 @@ export class ArFSPublicFileBuilder extends ArFSFileBuilder { this.dataTxId = new TransactionID(dataJSON.dataTxId); this.dataContentType = dataJSON.dataContentType ?? extToMime(this.name); - if ( - !this.name || - this.size === undefined || - !this.lastModifiedDate || - !this.dataTxId || - !this.dataContentType || - !(this.entityType === 'file') - ) { - throw new Error('Invalid file state'); - } + const fileBuilderValidation = new FileBuilderValidation(); + fileBuilderValidation.validateFileProperties(this); + fileBuilderValidation.throwIfMissingProperties(); + this.parseCustomMetaDataFromDataJson(dataJSON); return Promise.resolve( @@ -188,6 +182,10 @@ export class ArFSPrivateFileBuilder extends ArFSFileBuilder { const dataBuffer = Buffer.from(txData); const fileKey = this.fileKey ?? (await deriveFileKey(`${this.fileId}`, this.driveKey)); + if (!fileKey) { + throw new InvalidFileStateException(['fileKey']); + } + const decryptedFileBuffer: Buffer = await fileDecrypt(this.cipherIV, fileKey, dataBuffer); const decryptedFileString: string = await Utf8ArrayToStr(decryptedFileBuffer); const decryptedFileJSON: FileMetaDataTransactionData = await JSON.parse(decryptedFileString); @@ -199,17 +197,9 @@ export class ArFSPrivateFileBuilder extends ArFSFileBuilder { this.dataTxId = new TransactionID(decryptedFileJSON.dataTxId); this.dataContentType = decryptedFileJSON.dataContentType ?? extToMime(this.name); - if ( - !this.name || - this.size === undefined || - !this.lastModifiedDate || - !this.dataTxId || - !this.dataContentType || - !fileKey || - !(this.entityType === 'file') - ) { - throw new Error('Invalid file state'); - } + const fileBuilderValidation = new FileBuilderValidation(); + fileBuilderValidation.validateFileProperties(this); + fileBuilderValidation.throwIfMissingProperties(); this.parseCustomMetaDataFromDataJson(decryptedFileJSON); diff --git a/src/arfs/arfsdao.ts b/src/arfs/arfsdao.ts index 7b25d7bc..341cb692 100644 --- a/src/arfs/arfsdao.ts +++ b/src/arfs/arfsdao.ts @@ -179,6 +179,7 @@ import { assertDataRootsMatch, rePrepareV2Tx } from '../utils/arfsdao_utils'; import { ArFSDataToUpload, ArFSFolderToUpload, DrivePrivacy, errorMessage } from '../exports'; import { Turbo, TurboCachesResponse } from './turbo'; import { ArweaveSigner } from 'arbundles/src/signing'; +import { InvalidFileStateException } from '../types/exceptions'; /** Utility class for holding the driveId and driveKey of a new drive */ export class PrivateDriveKeyData { @@ -1583,21 +1584,31 @@ export class ArFSDAO extends ArFSDAOAnonymous { const transactions = await this.gatewayApi.gqlRequest(gqlQuery); const { edges } = transactions; hasNextPage = transactions.pageInfo.hasNextPage; - const files: Promise[] = edges.map(async (edge: GQLEdgeInterface) => { - const { node } = edge; - cursor = edge.cursor; - const fileBuilder = ArFSPrivateFileBuilder.fromArweaveNode(node, this.gatewayApi, driveKey); - // Build the file so that we don't add something invalid to the cache - const file = await fileBuilder.build(node); - const fileKey: FileKey = await deriveFileKey(`${file.fileId}`, driveKey); - const cacheKey = { - fileId: file.fileId, - owner, - fileKey - }; - return this.caches.privateFileCache.put(cacheKey, Promise.resolve(file)); + const files: Promise[] = edges.map(async (edge: GQLEdgeInterface) => { + try { + const { node } = edge; + cursor = edge.cursor; + const fileBuilder = ArFSPrivateFileBuilder.fromArweaveNode(node, this.gatewayApi, driveKey); + // Build the file so that we don't add something invalid to the cache + const file = await fileBuilder.build(node); + const fileKey: FileKey = await deriveFileKey(`${file.fileId}`, driveKey); + const cacheKey = { + fileId: file.fileId, + owner, + fileKey + }; + return this.caches.privateFileCache.put(cacheKey, Promise.resolve(file)); + } catch (e) { + if (e instanceof InvalidFileStateException) { + console.error(`Error building file. Skipping... Error: ${e}`); + return null; + } + + throw e; + } }); - allFiles.push(...(await Promise.all(files))); + const validFiles = (await Promise.all(files)).filter((f) => f !== null) as ArFSPrivateFile[]; + allFiles.push(...validFiles); } return latestRevisionsOnly ? allFiles.filter(latestRevisionFilter) : allFiles; } diff --git a/src/arfs/arfsdao_anonymous.ts b/src/arfs/arfsdao_anonymous.ts index 024e2deb..e76f79e7 100644 --- a/src/arfs/arfsdao_anonymous.ts +++ b/src/arfs/arfsdao_anonymous.ts @@ -27,6 +27,7 @@ import { alphabeticalOrder } from '../utils/sort_functions'; import { ArFSPublicFileWithPaths, ArFSPublicFolderWithPaths, publicEntityWithPathsFactory } from '../exports'; import { gatewayUrlForArweave } from '../utils/common'; import { GatewayAPI } from '../utils/gateway_api'; +import { InvalidFileStateException } from '../types/exceptions'; export abstract class ArFSDAOType { protected abstract readonly arweave: Arweave; @@ -267,16 +268,34 @@ export class ArFSDAOAnonymous extends ArFSDAOType { const transactions = await this.gatewayApi.gqlRequest(gqlQuery); const { edges } = transactions; hasNextPage = transactions.pageInfo.hasNextPage; - const files: Promise[] = edges.map(async (edge: GQLEdgeInterface) => { - const { node } = edge; - cursor = edge.cursor; - const fileBuilder = ArFSPublicFileBuilder.fromArweaveNode(node, this.gatewayApi); - const file = await fileBuilder.build(node); - const cacheKey = { fileId: file.fileId, owner }; - allFiles.push(file); - return this.caches.publicFileCache.put(cacheKey, Promise.resolve(file)); + const files: Promise[] = edges.map(async (edge: GQLEdgeInterface) => { + try { + const { node } = edge; + cursor = edge.cursor; + const fileBuilder = ArFSPublicFileBuilder.fromArweaveNode(node, this.gatewayApi); + const file = await fileBuilder.build(node); + const cacheKey = { fileId: file.fileId, owner }; + allFiles.push(file); + return this.caches.publicFileCache.put(cacheKey, Promise.resolve(file)); + } catch (e) { + // If the file is broken, skip it + if (e instanceof SyntaxError) { + console.error(`Error building file. Skipping... Error: ${e}`); + return null; + } + + if (e instanceof InvalidFileStateException) { + console.error(`Error building file. Skipping... Error: ${e}`); + return null; + } + + throw e; + } }); - await Promise.all(files); + + const validFiles = (await Promise.all(files)).filter((f) => f !== null) as ArFSPublicFile[]; + + allFiles.push(...validFiles); } return latestRevisionsOnly ? allFiles.filter(latestRevisionFilter) : allFiles; } diff --git a/src/types/exceptions.ts b/src/types/exceptions.ts new file mode 100644 index 00000000..3ad89e9c --- /dev/null +++ b/src/types/exceptions.ts @@ -0,0 +1,36 @@ +import { ArFSPrivateFileBuilder, ArFSPublicFileBuilder } from '../exports'; + +// InvalidFileStateException +export class InvalidFileStateException extends Error { + readonly missingProperties: string[]; + + constructor(missingProperties: string[]) { + const message = `Invalid file state. Missing required properties: ${missingProperties.join(', ')}`; + super(message); + this.missingProperties = missingProperties; + this.name = 'InvalidFileStateException'; + } +} + +export class FileBuilderValidation { + private missingProperties: string[] = []; + + validateFileProperties(builder: ArFSPublicFileBuilder | ArFSPrivateFileBuilder) { + if (!builder.name) this.missingProperties.push('name'); + if (builder.size === undefined) this.missingProperties.push('size'); + if (!builder.lastModifiedDate) this.missingProperties.push('lastModifiedDate'); + if (!builder.dataTxId) this.missingProperties.push('dataTxId'); + if (!builder.dataContentType) this.missingProperties.push('dataContentType'); + if (builder.entityType !== 'file') this.missingProperties.push('entityType'); + } + + throwIfMissingProperties() { + if (this.missingProperties.length > 0) { + throw new InvalidFileStateException(this.missingProperties); + } + } + + reset() { + this.missingProperties = []; + } +} From 0855aa057c722c5ba3157b4783d6ec3d6a55f7ad Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:52:08 -0300 Subject: [PATCH 2/3] fix(list-folders) --- src/arfs/arfsdao.ts | 2 +- src/arfs/arfsdao_anonymous.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/arfs/arfsdao.ts b/src/arfs/arfsdao.ts index 341cb692..a45574e1 100644 --- a/src/arfs/arfsdao.ts +++ b/src/arfs/arfsdao.ts @@ -1584,7 +1584,7 @@ export class ArFSDAO extends ArFSDAOAnonymous { const transactions = await this.gatewayApi.gqlRequest(gqlQuery); const { edges } = transactions; hasNextPage = transactions.pageInfo.hasNextPage; - const files: Promise[] = edges.map(async (edge: GQLEdgeInterface) => { + const files: Promise[] = edges.map(async (edge: GQLEdgeInterface) => { try { const { node } = edge; cursor = edge.cursor; diff --git a/src/arfs/arfsdao_anonymous.ts b/src/arfs/arfsdao_anonymous.ts index e76f79e7..f983c553 100644 --- a/src/arfs/arfsdao_anonymous.ts +++ b/src/arfs/arfsdao_anonymous.ts @@ -275,7 +275,6 @@ export class ArFSDAOAnonymous extends ArFSDAOType { const fileBuilder = ArFSPublicFileBuilder.fromArweaveNode(node, this.gatewayApi); const file = await fileBuilder.build(node); const cacheKey = { fileId: file.fileId, owner }; - allFiles.push(file); return this.caches.publicFileCache.put(cacheKey, Promise.resolve(file)); } catch (e) { // If the file is broken, skip it From 053c657f870255cbb179e5db6e475d0c4b0e0350 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 20 Nov 2024 08:52:38 -0300 Subject: [PATCH 3/3] test(arfs dao anonymous) - implement tests for `getPublicFilesWithParentFolderIds,` ensuring the broken files are skipped when having a bad state --- .../arfs_builders/arfs_file_builders.test.ts | 7 +- src/arfs/arfsdao_anonymous.test.ts | 287 +++++++++++++++++- 2 files changed, 289 insertions(+), 5 deletions(-) diff --git a/src/arfs/arfs_builders/arfs_file_builders.test.ts b/src/arfs/arfs_builders/arfs_file_builders.test.ts index 1ffd40d6..068f89dd 100644 --- a/src/arfs/arfs_builders/arfs_file_builders.test.ts +++ b/src/arfs/arfs_builders/arfs_file_builders.test.ts @@ -1,11 +1,14 @@ import { expect } from 'chai'; import { stub } from 'sinon'; -import { fakeArweave, stubTxID } from '../../../tests/stubs'; +import { fakeArweave, stubTxID, stubTxIDAlt } from '../../../tests/stubs'; import { expectAsyncErrorThrow } from '../../../tests/test_helpers'; -import { EntityKey, GQLNodeInterface } from '../../types'; +import { EID, EntityKey, GQLNodeInterface } from '../../types'; import { gatewayUrlForArweave } from '../../utils/common'; import { GatewayAPI } from '../../utils/gateway_api'; import { ArFSPrivateFileBuilder, ArFSPublicFileBuilder } from './arfs_file_builders'; +import { ArFSDAOAnonymous } from '../arfsdao_anonymous'; +import { ADDR, DriveID, FolderID } from '../../types'; +import { stub, SinonStub } from 'sinon'; const gatewayApi = new GatewayAPI({ gatewayUrl: gatewayUrlForArweave(fakeArweave), diff --git a/src/arfs/arfsdao_anonymous.test.ts b/src/arfs/arfsdao_anonymous.test.ts index 1cc94ba9..4c6b1f62 100644 --- a/src/arfs/arfsdao_anonymous.test.ts +++ b/src/arfs/arfsdao_anonymous.test.ts @@ -1,15 +1,19 @@ +/* eslint-disable prettier/prettier */ import Arweave from 'arweave'; import { expect } from 'chai'; -import { stub } from 'sinon'; +import { stub, SinonStub } from 'sinon'; import { stubArweaveAddress, stubEntityID, stubEntityIDAlt, stubPublicDrive, stubPublicFile, - stubPublicFolder + stubPublicFolder, + stubTxID, + stubTxIDAlt, + stubTxIDAltTwo } from '../../tests/stubs'; -import { ArweaveAddress, DriveID, EntityID } from '../types'; +import { ADDR, ArweaveAddress, DriveID, EID, EntityID } from '../types'; import { ArFSAnonymousCache, ArFSDAOAnonymous, @@ -19,6 +23,7 @@ import { } from './arfsdao_anonymous'; import { ArFSPublicDrive, ArFSPublicFile, ArFSPublicFolder } from './arfs_entities'; import { ArFSEntityCache } from './arfs_entity_cache'; +import { ArFSPublicFileBuilder } from './arfs_builders/arfs_file_builders'; const fakeArweave = Arweave.init({ host: 'localhost', @@ -226,4 +231,280 @@ describe('ArFSDAOAnonymous class', () => { // }); }); }); + + describe('getPublicFilesWithParentFolderIds', () => { + let dao: ArFSDAOAnonymous; + let gqlRequestStub: SinonStub; + let ArFSPublicFileBuilderStub: SinonStub; + + beforeEach(() => { + dao = new ArFSDAOAnonymous(fakeArweave); + gqlRequestStub = stub(dao['gatewayApi'], 'gqlRequest'); + ArFSPublicFileBuilderStub = stub(ArFSPublicFileBuilder.prototype, 'getDataForTxID'); + }); + + afterEach(() => { + gqlRequestStub.restore(); + ArFSPublicFileBuilderStub.restore(); + }); + + it('returns expected files for given folder IDs', async () => { + // Mock GQL response + const mockGQLResponse = { + edges: [ + { + cursor: 'cursor1', + node: { + id: `${stubTxID}`, + tags: [ + { name: 'App-Name', value: 'ArDrive-CLI' }, + { name: 'App-Version', value: '1.2.0' }, + { name: 'ArFS', value: '0.11' }, + { name: 'Content-Type', value: 'application/json' }, + { name: 'Drive-Id', value: 'e93cf9c4-5f20-4d7a-87c4-034777cbb51e' }, + { name: 'Entity-Type', value: 'file' }, + { name: 'Unix-Time', value: '1639073846' }, + { name: 'Parent-Folder-Id', value: '6c312b3e-4778-4a18-8243-f2b346f5e7cb' }, + { name: 'File-Id', value: '9f7038c7-26bd-4856-a843-8de24b828d4e' } + ], + owner: { address: 'vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI' } + } + } + ], + pageInfo: { + hasNextPage: false + } + }; + + gqlRequestStub.resolves(mockGQLResponse); + + // Add stub for getDataForTxID + const stubFileGetDataResult = Buffer.from( + JSON.stringify({ + name: '2', + size: 2048, + lastModifiedDate: 1639073634269, + dataTxId: 'yAogaGWWYgWO5xWZevb45Y7YRp7E9iDsvkJvfR7To9c', + dataContentType: 'unknown' + }) + ); + + // Stub the getDataForTxID method on the builder + ArFSPublicFileBuilderStub.resolves(stubFileGetDataResult); + + const folderIds = [EID('6c312b3e-4778-4a18-8243-f2b346f5e7cb')]; + const owner = ADDR('vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI'); + const driveId = EID('e93cf9c4-5f20-4d7a-87c4-034777cbb51e'); + + const files = await dao.getPublicFilesWithParentFolderIds(folderIds, owner, driveId, true); + + expect(files).to.have.lengthOf(1); + expect(`${files[0].fileId}`).to.equal('9f7038c7-26bd-4856-a843-8de24b828d4e'); + expect(`${files[0].driveId}`).to.equal('e93cf9c4-5f20-4d7a-87c4-034777cbb51e'); + expect(`${files[0].parentFolderId}`).to.equal('6c312b3e-4778-4a18-8243-f2b346f5e7cb'); + }); + + it('handles pagination correctly', async () => { + const fileId1 = '9f7038c7-26bd-4856-a843-8de24b828d4e'; + const fileId2 = '1f7038c7-26bd-4856-a843-8de24b828d4e'; + + // Mock two pages of GQL responses + const mockGQLResponse1 = { + edges: [ + { + cursor: 'cursor1', + node: { + id: `${stubTxID}`, + tags: [ + { name: 'App-Name', value: 'ArDrive-CLI' }, + { name: 'App-Version', value: '1.2.0' }, + { name: 'ArFS', value: '0.11' }, + { name: 'Content-Type', value: 'application/json' }, + { name: 'Drive-Id', value: 'e93cf9c4-5f20-4d7a-87c4-034777cbb51e' }, + { name: 'Entity-Type', value: 'file' }, + { name: 'Unix-Time', value: '1639073846' }, + { name: 'Parent-Folder-Id', value: '6c312b3e-4778-4a18-8243-f2b346f5e7cb' }, + { name: 'File-Id', value: fileId1 } + ], + owner: { address: 'vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI' } + } + } + ], + pageInfo: { + hasNextPage: true + } + }; + + const mockGQLResponse2 = { + edges: [ + { + cursor: 'cursor2', + node: { + id: `${stubTxIDAlt}`, + tags: [ + { name: 'App-Name', value: 'ArDrive-CLI' }, + { name: 'App-Version', value: '1.2.0' }, + { name: 'ArFS', value: '0.11' }, + { name: 'Content-Type', value: 'application/json' }, + { name: 'Drive-Id', value: 'e93cf9c4-5f20-4d7a-87c4-034777cbb51e' }, + { name: 'Entity-Type', value: 'file' }, + { name: 'Unix-Time', value: '1639073846' }, + { name: 'Parent-Folder-Id', value: '6c312b3e-4778-4a18-8243-f2b346f5e7cb' }, + { name: 'File-Id', value: fileId2 } + ], + owner: { address: 'vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI' } + } + } + ], + pageInfo: { + hasNextPage: false + } + }; + + gqlRequestStub.onFirstCall().resolves(mockGQLResponse1); + gqlRequestStub.onSecondCall().resolves(mockGQLResponse2); + + const folderIds = [EID('6c312b3e-4778-4a18-8243-f2b346f5e7cb')]; + const owner = ADDR('vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI'); + const driveId = EID('e93cf9c4-5f20-4d7a-87c4-034777cbb51e'); + + // Add stub for getDataForTxID + const stubFileGetDataResult = Buffer.from( + JSON.stringify({ + name: '2', + size: 2048, + lastModifiedDate: 1639073634269, + dataTxId: 'yAogaGWWYgWO5xWZevb45Y7YRp7E9iDsvkJvfR7To9c', + dataContentType: 'unknown' + }) + ); + + ArFSPublicFileBuilderStub.withArgs(stubTxID).resolves(stubFileGetDataResult); + ArFSPublicFileBuilderStub.withArgs(stubTxIDAlt).resolves(stubFileGetDataResult); + + const files = await dao.getPublicFilesWithParentFolderIds(folderIds, owner, driveId, true); + + expect(files).to.have.lengthOf(2); + expect(`${files[0].fileId}`).to.equal(fileId1); + expect(`${files[1].fileId}`).to.equal(fileId2); + expect(gqlRequestStub.callCount).to.equal(2); + }); + + it('skips invalid files and continues processing', async () => { + const fileId1 = '9f7038c7-26bd-4856-a843-8de24b828d4e'; + const fileId2 = '1f7038c7-26bd-4856-a843-8de24b828d4e'; + const fileId3 = '2f7038c7-26bd-4856-a843-8de24b828d4e'; + + const mockGQLResponse = { + edges: [ + { + cursor: 'cursor1', + node: { + id: `${stubTxID}`, + tags: [ + { name: 'App-Name', value: 'ArDrive-CLI' }, + { name: 'App-Version', value: '1.2.0' }, + { name: 'ArFS', value: '0.11' }, + { name: 'Content-Type', value: 'application/json' }, + { name: 'Drive-Id', value: 'e93cf9c4-5f20-4d7a-87c4-034777cbb51e' }, + { name: 'Entity-Type', value: 'file' }, + { name: 'Unix-Time', value: '1639073846' }, + { name: 'Parent-Folder-Id', value: '6c312b3e-4778-4a18-8243-f2b346f5e7cb' }, + { name: 'File-Id', value: fileId1 } + ], + owner: { address: 'vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI' } + } + }, + { + cursor: 'cursor2', + node: { + id: `${stubTxIDAlt}`, + tags: [ + { name: 'App-Name', value: 'ArDrive-CLI' }, + { name: 'App-Version', value: '1.2.0' }, + { name: 'ArFS', value: '0.11' }, + { name: 'Content-Type', value: 'application/json' }, + { name: 'Drive-Id', value: 'e93cf9c4-5f20-4d7a-87c4-034777cbb51e' }, + { name: 'Entity-Type', value: 'file' }, + { name: 'Unix-Time', value: '1639073846' }, + { name: 'Parent-Folder-Id', value: '1c312b3e-4778-4a18-8243-f2b346f5e7cb' }, + { name: 'File-Id', value: fileId2 } + ], + owner: { address: 'vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI' } + } + }, + { + cursor: 'cursor3', + node: { + id: `${stubTxIDAltTwo}`, + tags: [ + { name: 'App-Name', value: 'ArDrive-CLI' }, + { name: 'App-Version', value: '1.2.0' }, + { name: 'ArFS', value: '0.11' }, + { name: 'Content-Type', value: 'application/json' }, + { name: 'Drive-Id', value: 'e93cf9c4-5f20-4d7a-87c4-034777cbb51e' }, + { name: 'Entity-Type', value: 'file' }, + { name: 'Unix-Time', value: '1639073846' }, + { name: 'Parent-Folder-Id', value: '1c312b3e-4778-4a18-8243-f2b346f5e7cb' }, + { name: 'File-Id', value: fileId3 } + ], + owner: { address: 'vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI' } + } + } + ], + pageInfo: { + hasNextPage: false + } + }; + gqlRequestStub.resolves(mockGQLResponse); + // Mock the getDataForTxID method to return valid metadata for the second file + const stubFileGetDataResultValid1 = Buffer.from( + JSON.stringify({ + name: '2', + size: 2048, + lastModifiedDate: 1639073634269, + dataTxId: 'yAogaGWWYgWO5xWZevb45Y7YRp7E9iDsvkJvfR7To9c', + dataContentType: 'application/json' + }) + ); + // Invalid file metadata missing dataContentType + const stubFileGetDataResultWithEmptyContentType = Buffer.from( + JSON.stringify({ + name: '2', + size: 2048, + lastModifiedDate: 1639073634269, + dataTxId: 'yAogaGWWYgWO5xWZevb45Y7YRp7E9iDsvkJvfR7To9c', + dataContentType: '' + }) + ); + // Valid file metadata + const stubFileGetDataResultValid2 = Buffer.from( + JSON.stringify({ + name: '2', + size: 2048, + lastModifiedDate: 1639073634269, + dataTxId: 'yAogaGWWYgWO5xWZevb45Y7YRp7E9iDsvkJvfR7To9c', + dataContentType: 'text/plain' + }) + ); + + // Valid file metadata + ArFSPublicFileBuilderStub.withArgs(stubTxID).resolves(stubFileGetDataResultValid1); + // Invalid file metadata missing dataContentType + ArFSPublicFileBuilderStub.withArgs(stubTxIDAlt).resolves(stubFileGetDataResultWithEmptyContentType); + // Valid file metadata + ArFSPublicFileBuilderStub.withArgs(stubTxIDAltTwo).resolves(stubFileGetDataResultValid2); + + const folderIds = [EID('6c312b3e-4778-4a18-8243-f2b346f5e7cb')]; + const owner = ADDR('vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI'); + const driveId = EID('e93cf9c4-5f20-4d7a-87c4-034777cbb51e'); + + const files = await dao.getPublicFilesWithParentFolderIds(folderIds, owner, driveId, true); + + // Verify that the invalid file was skipped + expect(files).to.have.lengthOf(2); + expect(`${files[0].fileId}`).to.equal(fileId1); + expect(`${files[1].fileId}`).to.equal(fileId3); + }); + }); });