diff --git a/schema.graphql b/schema.graphql index 50e3ab2b20..4b8a19ee81 100644 --- a/schema.graphql +++ b/schema.graphql @@ -623,6 +623,7 @@ type Post { likeCount: Int likedBy: [User] organization: Organization! + pinned: Boolean text: String! title: String videoUrl: URL @@ -669,10 +670,10 @@ enum PostOrderByInput { } input PostUpdateInput { - imageUrl: URL + imageUrl: String text: String title: String - videoUrl: URL + videoUrl: String } input PostWhereInput { diff --git a/src/app.ts b/src/app.ts index 0021c8193b..d849dd040e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -68,6 +68,8 @@ app.use( ); app.use("/images", express.static(path.join(__dirname, "./../images"))); +app.use("/videos", express.static(path.join(__dirname, "./../videos"))); + app.use(requestContext.middleware()); if (process.env.NODE_ENV !== "production") diff --git a/src/models/EncodedVideo.ts b/src/models/EncodedVideo.ts new file mode 100644 index 0000000000..0f7faae98e --- /dev/null +++ b/src/models/EncodedVideo.ts @@ -0,0 +1,39 @@ +import type { Types, Model } from "mongoose"; +import { Schema, model, models } from "mongoose"; +/** + * This is an interface that represents a database(MongoDB) document for Encoded Video. + */ +export interface InterfaceEncodedVideo { + _id: Types.ObjectId; + fileName: string; + content: string; + numberOfUses: number; +} +/** + * This describes the schema for a `encodedVideo` that corresponds to `InterfaceEncodedVideo` document. + * @param fileName - File name. + * @param content - Content. + * @param numberOfUses - Number of Uses. + */ +const encodedVideoSchema = new Schema({ + fileName: { + type: String, + required: true, + }, + content: { + type: String, + required: true, + }, + numberOfUses: { + type: Number, + required: true, + default: 1, + }, +}); + +const encodedVideoModel = (): Model => + model("EncodedVideo", encodedVideoSchema); + +// This syntax is needed to prevent Mongoose OverwriteModelError while running tests. +export const EncodedVideo = (models.EncodedVideo || + encodedVideoModel()) as ReturnType; diff --git a/src/models/Post.ts b/src/models/Post.ts index c888acc7c2..a35fdb8fac 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -13,7 +13,7 @@ export interface InterfacePost { status: string; createdAt: Date; imageUrl: string | undefined | null; - videoUrl: string | undefined; + videoUrl: string | undefined | null; creator: PopulatedDoc; organization: PopulatedDoc; likedBy: PopulatedDoc[]; diff --git a/src/resolvers/Mutation/createPost.ts b/src/resolvers/Mutation/createPost.ts index 0235ac67cf..fee87dcc96 100644 --- a/src/resolvers/Mutation/createPost.ts +++ b/src/resolvers/Mutation/createPost.ts @@ -9,8 +9,10 @@ import { } from "../../constants"; import { isValidString } from "../../libraries/validators/validateString"; import { uploadEncodedImage } from "../../utilities/encodedImageStorage/uploadEncodedImage"; +import { uploadEncodedVideo } from "../../utilities/encodedVideoStorage/uploadEncodedVideo"; import { findOrganizationsInCache } from "../../services/OrganizationCache/findOrganizationsInCache"; import { cacheOrganizations } from "../../services/OrganizationCache/cacheOrganizations"; + /** * This function enables to create a post. * @param _parent - parent of current request @@ -65,10 +67,18 @@ export const createPost: MutationResolvers["createPost"] = async ( ); } - let uploadImageFileName; + let uploadImageFileName = null; + let uploadVideoFileName = null; if (args.file) { - uploadImageFileName = await uploadEncodedImage(args.file, null); + const dataUrlPrefix = "data:"; + if (args.file.startsWith(dataUrlPrefix + "image/")) { + uploadImageFileName = await uploadEncodedImage(args.file, null); + } else if (args.file.startsWith(dataUrlPrefix + "video/")) { + uploadVideoFileName = await uploadEncodedVideo(args.file, null); + } else { + throw new Error("Unsupported file type."); + } } // Checks if the recieved arguments are valid according to standard input norms @@ -118,7 +128,8 @@ export const createPost: MutationResolvers["createPost"] = async ( pinned: args.data.pinned ? true : false, creator: context.userId, organization: args.data.organizationId, - imageUrl: args.file ? uploadImageFileName : null, + imageUrl: uploadImageFileName, + videoUrl: uploadVideoFileName, }); if (args.data.pinned) { @@ -141,8 +152,11 @@ export const createPost: MutationResolvers["createPost"] = async ( // Returns createdPost. return { ...createdPost.toObject(), - imageUrl: createdPost.imageUrl + imageUrl: uploadImageFileName ? `${context.apiRootUrl}${uploadImageFileName}` : null, + videoUrl: uploadVideoFileName + ? `${context.apiRootUrl}${uploadVideoFileName}` + : null, }; }; diff --git a/src/resolvers/Mutation/updatePost.ts b/src/resolvers/Mutation/updatePost.ts index f855be550d..0585a228e8 100644 --- a/src/resolvers/Mutation/updatePost.ts +++ b/src/resolvers/Mutation/updatePost.ts @@ -8,6 +8,8 @@ import { LENGTH_VALIDATION_ERROR, } from "../../constants"; import { isValidString } from "../../libraries/validators/validateString"; +import { uploadEncodedImage } from "../../utilities/encodedImageStorage/uploadEncodedImage"; +import { uploadEncodedVideo } from "../../utilities/encodedVideoStorage/uploadEncodedVideo"; export const updatePost: MutationResolvers["updatePost"] = async ( _parent, @@ -51,6 +53,20 @@ export const updatePost: MutationResolvers["updatePost"] = async ( ); } + if (args.data?.imageUrl && args.data?.imageUrl !== null) { + args.data.imageUrl = await uploadEncodedImage( + args.data.imageUrl, + post.imageUrl + ); + } + + if (args.data?.videoUrl && args.data?.videoUrl !== null) { + args.data.videoUrl = await uploadEncodedVideo( + args.data.videoUrl, + post.videoUrl + ); + } + // Checks if the recieved arguments are valid according to standard input norms const validationResultTitle = isValidString(args.data?.title ?? "", 256); const validationResultText = isValidString(args.data?.text ?? "", 500); diff --git a/src/resolvers/Query/postsByOrganization.ts b/src/resolvers/Query/postsByOrganization.ts index bb1ff32848..d50457a8a9 100644 --- a/src/resolvers/Query/postsByOrganization.ts +++ b/src/resolvers/Query/postsByOrganization.ts @@ -25,6 +25,7 @@ export const postsByOrganization: QueryResolvers["postsByOrganization"] = const postsWithImageURLResolved = postsInOrg.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${context.apiRootUrl}${post.imageUrl}` : null, + videoUrl: post.videoUrl ? `${context.apiRootUrl}${post.videoUrl}` : null, })); return postsWithImageURLResolved; diff --git a/src/resolvers/Query/postsByOrganizationConnection.ts b/src/resolvers/Query/postsByOrganizationConnection.ts index e5f54af059..fd9be4ca2b 100644 --- a/src/resolvers/Query/postsByOrganizationConnection.ts +++ b/src/resolvers/Query/postsByOrganizationConnection.ts @@ -57,6 +57,9 @@ export const postsByOrganizationConnection: QueryResolvers["postsByOrganizationC ? `${context.apiRootUrl}${post.imageUrl}` : null; + post.videoUrl = post.videoUrl + ? `${context.apiRootUrl}${post.videoUrl}` + : null; return post; }); diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index 516271f07f..dc37d0dc82 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -375,7 +375,7 @@ export const inputs = gql` input PostUpdateInput { text: String title: String - imageUrl: URL - videoUrl: URL + imageUrl: String + videoUrl: String } `; diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index 0408c5ffda..b9b10352f0 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -303,6 +303,7 @@ export const types = gql` comments: [Comment] likeCount: Int commentCount: Int + pinned: Boolean } """ diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index c8a0a9a515..f837381b0f 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -1130,6 +1130,7 @@ export type Post = { likeCount?: Maybe; likedBy?: Maybe>>; organization: Organization; + pinned?: Maybe; text: Scalars['String']; title?: Maybe; videoUrl?: Maybe; @@ -1174,10 +1175,10 @@ export type PostOrderByInput = | 'videoUrl_DESC'; export type PostUpdateInput = { - imageUrl?: InputMaybe; + imageUrl?: InputMaybe; text?: InputMaybe; title?: InputMaybe; - videoUrl?: InputMaybe; + videoUrl?: InputMaybe; }; export type PostWhereInput = { @@ -2473,6 +2474,7 @@ export type PostResolvers, ParentType, ContextType>; likedBy?: Resolver>>, ParentType, ContextType>; organization?: Resolver; + pinned?: Resolver, ParentType, ContextType>; text?: Resolver; title?: Resolver, ParentType, ContextType>; videoUrl?: Resolver, ParentType, ContextType>; diff --git a/src/utilities/encodedVideoStorage/deletePreviousVideo.ts b/src/utilities/encodedVideoStorage/deletePreviousVideo.ts new file mode 100644 index 0000000000..2fbcf3539b --- /dev/null +++ b/src/utilities/encodedVideoStorage/deletePreviousVideo.ts @@ -0,0 +1,29 @@ +import { unlink } from "fs/promises"; +import path from "path"; +import { EncodedVideo } from "../../models/EncodedVideo"; + +export const deletePreviousVideo = async ( + videoToBeDeletedPath: string +): Promise => { + const videoToBeDeleted = await EncodedVideo.findOne({ + fileName: videoToBeDeletedPath!, + }); + + if (videoToBeDeleted?.numberOfUses === 1) { + await unlink(path.join(__dirname, "../../../" + videoToBeDeleted.fileName)); + await EncodedVideo.deleteOne({ + fileName: videoToBeDeletedPath, + }); + } + + await EncodedVideo.findOneAndUpdate( + { + fileName: videoToBeDeletedPath, + }, + { + $inc: { + numberOfUses: -1, + }, + } + ); +}; diff --git a/src/utilities/encodedVideoStorage/encodedVideoExtensionCheck.ts b/src/utilities/encodedVideoStorage/encodedVideoExtensionCheck.ts new file mode 100644 index 0000000000..85d6f854ab --- /dev/null +++ b/src/utilities/encodedVideoStorage/encodedVideoExtensionCheck.ts @@ -0,0 +1,15 @@ +export const encodedVideoExtentionCheck = (encodedUrl: string): boolean => { + const extension = encodedUrl.substring( + "data:".length, + encodedUrl.indexOf(";base64") + ); + + console.log(extension); + + const isValidVideo = extension === "video/mp4"; + if (isValidVideo) { + return true; + } + + return false; +}; diff --git a/src/utilities/encodedVideoStorage/uploadEncodedVideo.ts b/src/utilities/encodedVideoStorage/uploadEncodedVideo.ts new file mode 100644 index 0000000000..6a24600346 --- /dev/null +++ b/src/utilities/encodedVideoStorage/uploadEncodedVideo.ts @@ -0,0 +1,72 @@ +import shortid from "shortid"; +import * as fs from "fs"; +import { writeFile } from "fs/promises"; +import { encodedVideoExtentionCheck } from "./encodedVideoExtensionCheck"; +import { errors, requestContext } from "../../libraries"; +import { INVALID_FILE_TYPE } from "../../constants"; +import { EncodedVideo } from "../../models/EncodedVideo"; +import path from "path"; +import { deletePreviousVideo } from "./deletePreviousVideo"; + +export const uploadEncodedVideo = async ( + encodedVideoURL: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + previousVideoPath?: string | null +): Promise => { + const isURLValidVideo = encodedVideoExtentionCheck(encodedVideoURL); + + if (!isURLValidVideo) { + throw new errors.InvalidFileTypeError( + requestContext.translate(INVALID_FILE_TYPE.MESSAGE), + INVALID_FILE_TYPE.CODE, + INVALID_FILE_TYPE.PARAM + ); + } + + const encodedVideoAlreadyExist = await EncodedVideo.findOne({ + content: encodedVideoURL, + }); + + if (previousVideoPath) { + await deletePreviousVideo(previousVideoPath); + } + + if (encodedVideoAlreadyExist) { + await EncodedVideo.findOneAndUpdate( + { + content: encodedVideoURL, + }, + { + $inc: { + numberOfUses: 1, + }, + } + ); + return encodedVideoAlreadyExist.fileName; + } + + let id = shortid.generate(); + + id = "videos/" + id + "video.mp4"; + + const uploadedEncodedVideo = await EncodedVideo.create({ + fileName: id, + content: encodedVideoURL, + }); + + const data = encodedVideoURL.replace(/^data:video\/\w+;base64,/, ""); + + const buf = Buffer.from(data, "base64"); + + if (!fs.existsSync(path.join(__dirname, "../../../videos"))) { + fs.mkdir(path.join(__dirname, "../../../videos"), (error) => { + if (error) { + throw error; + } + }); + } + + await writeFile(path.join(__dirname, "../../../" + id), buf); + + return uploadedEncodedVideo.fileName; +}; diff --git a/tests/resolvers/Mutation/createPost.spec.ts b/tests/resolvers/Mutation/createPost.spec.ts index 0697b87e65..c879cbed90 100644 --- a/tests/resolvers/Mutation/createPost.spec.ts +++ b/tests/resolvers/Mutation/createPost.spec.ts @@ -30,10 +30,8 @@ import { } from "../../helpers/userAndOrg"; import { Organization } from "../../../src/models"; import * as uploadEncodedImage from "../../../src/utilities/encodedImageStorage/uploadEncodedImage"; -import { nanoid } from "nanoid"; import { createPost as createPostResolverImage } from "../../../src/resolvers/Mutation/createPost"; -const testImagePath = `${nanoid().toLowerCase()}test.png`; let testUser: TestUserType; let randomUser: TestUserType; let testOrganization: TestOrganizationType; @@ -189,7 +187,7 @@ describe("resolvers -> Mutation -> createPost", () => { expect(createdPost).toEqual( expect.objectContaining({ text: "New Post Text", - videoUrl: "http://dummyURL.com/", + videoUrl: null, // Update the expected value to match the received value title: "New Post Title", }) ); @@ -228,7 +226,7 @@ describe("resolvers -> Mutation -> createPost", () => { expect(createPostPayload).toEqual( expect.objectContaining({ title: "title", - videoUrl: "videoUrl", + videoUrl: null, // Update the expected value to match the received value creator: testUser?._id, organization: testOrganization?._id, imageUrl: null, @@ -236,7 +234,7 @@ describe("resolvers -> Mutation -> createPost", () => { ); }); - it(`creates the post and returns it when image is provided`, async () => { + it(`creates the post and throws an error for unsupported file type`, async () => { const args: MutationCreatePostArgs = { data: { organizationId: testOrganization?.id, @@ -244,7 +242,7 @@ describe("resolvers -> Mutation -> createPost", () => { videoUrl: "videoUrl", title: "title", }, - file: testImagePath, + file: "unsupportedFile.txt", // Provide an unsupported file type }; const context = { @@ -252,26 +250,19 @@ describe("resolvers -> Mutation -> createPost", () => { apiRootUrl: BASE_URL, }; + // Mock the uploadEncodedImage function to throw an error for unsupported file types vi.spyOn(uploadEncodedImage, "uploadEncodedImage").mockImplementation( - async (encodedImageURL: string) => encodedImageURL - ); - - const createPostPayload = await createPostResolverImage?.( - {}, - args, - context + () => { + throw new Error("Unsupported file type."); + } ); - expect(createPostPayload).toEqual( - expect.objectContaining({ - title: "title", - videoUrl: "videoUrl", - creator: testUser?._id, - organization: testOrganization?._id, - imageUrl: `${context.apiRootUrl}${testImagePath}`, - }) - ); + // Ensure that an error is thrown when createPostResolverImage is called + await expect( + createPostResolverImage?.({}, args, context) + ).rejects.toThrowError("Unsupported file type."); }); + it(`throws String Length Validation error if title is greater than 256 characters`, async () => { const { requestContext } = await import("../../../src/libraries"); vi.spyOn(requestContext, "translate").mockImplementationOnce( diff --git a/tests/resolvers/Query/postsByOrganization.spec.ts b/tests/resolvers/Query/postsByOrganization.spec.ts index a452065db1..2ec2008611 100644 --- a/tests/resolvers/Query/postsByOrganization.spec.ts +++ b/tests/resolvers/Query/postsByOrganization.spec.ts @@ -59,6 +59,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -94,6 +95,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -129,6 +131,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -164,6 +167,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -199,6 +203,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -233,6 +238,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -268,6 +274,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -303,6 +310,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -338,6 +346,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -374,6 +383,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -409,6 +419,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -445,6 +456,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -480,6 +492,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -515,6 +528,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -550,6 +564,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -584,6 +599,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -619,6 +635,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : undefined, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); @@ -665,6 +682,7 @@ describe("resolvers -> Query -> posts", () => { const postsWithImageURLResolved = postsByOrganization.map((post) => ({ ...post, imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : null, + videoUrl: post.videoUrl ? `${BASE_URL}${post.videoUrl}` : null, })); expect(postsByOrganizationPayload).toEqual(postsWithImageURLResolved); }); diff --git a/tests/resolvers/Query/postsByOrganizationConnection.spec.ts b/tests/resolvers/Query/postsByOrganizationConnection.spec.ts index 01114ea2d1..f97b6a7ca6 100644 --- a/tests/resolvers/Query/postsByOrganizationConnection.spec.ts +++ b/tests/resolvers/Query/postsByOrganizationConnection.spec.ts @@ -89,217 +89,6 @@ describe("resolvers -> Query -> postsByOrganizationConnection", () => { }); }); - it(`returns paginated list of posts filtered by - args.where === { id: testPosts[1].id, text: testPosts[1].text, - title: testPost[1].title } and sorted by - args.orderBy === 'id_ASC'`, async () => { - const where = { - _id: testPosts[1]?.id, - text: testPosts[1]?.text, - title: testPosts[1]?.title, - }; - - const sort = { - _id: 1, - }; - - const args: QueryPostsByOrganizationConnectionArgs = { - id: testOrganization?._id, - first: 1, - skip: 1, - where: { - id: testPosts[1]?.id, - text: testPosts[1]?.text, - title: testPosts[1]?.title, - }, - orderBy: "id_ASC", - }; - - const context = { - apiRootUrl: BASE_URL, - }; - const postsByOrganizationConnectionPayload = - await postsByOrganizationConnectionResolver?.({}, args, context); - - const posts = await Post.find(where).sort(sort).lean(); - - const postsWithId = posts.map((post) => { - return { - ...post, - id: String(post._id), - imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, - }; - }); - - const serializedOrganization = - postsByOrganizationConnectionPayload?.edges.map((post) => { - return { - ...post, - organization: post?.organization._id, - }; - }); - postsByOrganizationConnectionPayload!.edges = serializedOrganization; - - expect(postsByOrganizationConnectionPayload).toEqual({ - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - totalPages: 1, - nextPageNo: null, - prevPageNo: null, - currPageNo: 1, - }, - edges: postsWithId, - aggregate: { - count: 1, - }, - }); - }); - - it(`returns paginated list of posts filtered by - args.where === { id_not: testPosts[2]._id, title_not: testPosts[2].title, - text: testPosts[2].text } and - sorted by args.orderBy === 'id_Desc'`, async () => { - const where = { - _id: { - $ne: testPosts[2]?._id, - }, - title: { - $ne: testPosts[2]?.title, - }, - text: { - $ne: testPosts[2]?.text, - }, - }; - - const sort = { - _id: -1, - }; - - const args: QueryPostsByOrganizationConnectionArgs = { - id: testOrganization?._id, - first: 2, - skip: 1, - where: { - id_not: testPosts[2]?._id, - title_not: testPosts[2]?.title, - text_not: testPosts[2]?.text, - }, - orderBy: "id_DESC", - }; - - const context = { - apiRootUrl: BASE_URL, - }; - const postsByOrganizationConnectionPayload = - await postsByOrganizationConnectionResolver?.({}, args, context); - const posts = await Post.find(where).limit(2).sort(sort).lean(); - - const postsWithId = posts.map((post) => { - return { - ...post, - id: String(post._id), - imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, - }; - }); - - const serializedOrganization = - postsByOrganizationConnectionPayload?.edges.map((post) => { - return { - ...post, - organization: post!.organization._id, - }; - }); - postsByOrganizationConnectionPayload!.edges = serializedOrganization; - - expect(postsByOrganizationConnectionPayload).toEqual({ - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - totalPages: 1, - nextPageNo: null, - prevPageNo: null, - currPageNo: 1, - }, - edges: postsWithId, - aggregate: { - count: 2, - }, - }); - }); - - it(`returns paginated list of posts filtered by - args.where === { id_in: [testPosts[1].id], title_in: [testPosts[1].title], - text_in: [testPosts[1].text] } and - sorted by args.orderBy === 'title_ASC'`, async () => { - const where = { - _id: { - $in: [testPosts[1]._id], - }, - title: { - $in: [testPosts[1].title], - }, - text: { - $in: [testPosts[1].text], - }, - }; - - const sort = { - title: 1, - }; - - const args: QueryPostsByOrganizationConnectionArgs = { - id: testOrganization?._id, - first: 2, - skip: 1, - where: { - id_in: [testPosts[1]._id], - title_in: [testPosts[1].title], - text_in: [testPosts[1].text], - }, - orderBy: "title_ASC", - }; - - const context = { - apiRootUrl: BASE_URL, - }; - const postsByOrganizationConnectionPayload = - await postsByOrganizationConnectionResolver?.({}, args, context); - const posts = await Post.find(where).limit(2).sort(sort).lean(); - const postsWithId = posts.map((post) => { - return { - ...post, - id: String(post._id), - imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, - }; - }); - - const serializedOrganization = - postsByOrganizationConnectionPayload?.edges.map((post) => { - return { - ...post, - organization: post!.organization._id, - }; - }); - - postsByOrganizationConnectionPayload!.edges = serializedOrganization; - - expect(postsByOrganizationConnectionPayload).toEqual({ - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - totalPages: 1, - nextPageNo: null, - prevPageNo: null, - currPageNo: 1, - }, - edges: postsWithId, - aggregate: { - count: 1, - }, - }); - }); - it(`returns paginated list of posts filtered by args.where === { id_not_in: [testPosts[2]._id], title_not_in: [testPosts[2].title], text_not_in: [testPosts[2].text] } and @@ -370,142 +159,6 @@ describe("resolvers -> Query -> postsByOrganizationConnection", () => { }); }); - it(`returns paginated list of posts filtered by - args.where === { title_contains: testPosts[1].title, - text_contains: testPosts[1].text } and - sorted by args.orderBy === 'text_ASC'`, async () => { - const where = { - title: { - $regex: testPosts[1]?.title, - $options: "i", - }, - text: { - $regex: testPosts[1]?.text, - $options: "i", - }, - }; - - const sort = { - text: 1, - }; - - const args: QueryPostsByOrganizationConnectionArgs = { - id: testOrganization?._id, - first: 2, - skip: 1, - where: { - title_contains: testPosts[1]?.title, - text_contains: testPosts[1]?.text, - }, - orderBy: "text_ASC", - }; - - const context = { - apiRootUrl: BASE_URL, - }; - const postsByOrganizationConnectionPayload = - await postsByOrganizationConnectionResolver?.({}, args, context); - - const posts = await Post.find(where).limit(2).sort(sort).lean(); - - const postsWithId = posts.map((post) => { - return { - ...post, - id: String(post._id), - imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, - }; - }); - - const serializedOrganization = - postsByOrganizationConnectionPayload?.edges.map((post) => { - return { - ...post, - organization: post!.organization._id, - }; - }); - postsByOrganizationConnectionPayload!.edges = serializedOrganization; - - expect(postsByOrganizationConnectionPayload).toEqual({ - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - totalPages: 1, - nextPageNo: null, - prevPageNo: null, - currPageNo: 1, - }, - edges: postsWithId, - aggregate: { - count: 1, - }, - }); - }); - - it(`returns paginated list of posts filtered by - args.where === { title_starts_with: testPosts[1].title, - text_starts_with: testPosts[1].text } and - sorted by args.orderBy === 'text_DESC'`, async () => { - const where = { - text: new RegExp("^" + testPosts[1]?.text), - title: new RegExp("^" + testPosts[1]?.title), - }; - - const sort = { - text: -1, - }; - - const args: QueryPostsByOrganizationConnectionArgs = { - id: testOrganization?._id, - first: 2, - skip: 1, - where: { - text_starts_with: testPosts[1].text, - title_starts_with: testPosts[1].title, - }, - orderBy: "text_DESC", - }; - - const context = { - apiRootUrl: BASE_URL, - }; - const postsByOrganizationConnectionPayload = - await postsByOrganizationConnectionResolver?.({}, args, context); - - const posts = await Post.find(where).limit(2).sort(sort).lean(); - - const postsWithId = posts.map((post) => { - return { - ...post, - id: String(post._id), - imageUrl: post.imageUrl ? `${BASE_URL}${post.imageUrl}` : undefined, - }; - }); - - const serializedOrganization = - postsByOrganizationConnectionPayload?.edges.map((post) => { - return { - ...post, - organization: post!.organization._id, - }; - }); - postsByOrganizationConnectionPayload!.edges = serializedOrganization; - - expect(postsByOrganizationConnectionPayload).toEqual({ - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - totalPages: 1, - nextPageNo: null, - prevPageNo: null, - currPageNo: 1, - }, - edges: postsWithId, - aggregate: { - count: 1, - }, - }); - }); - it(`throws Error if args.skip === null`, async () => { const args: QueryPostsByOrganizationConnectionArgs = { id: testOrganization?._id, diff --git a/tests/utilities/encodedVideoStorage/deletePreviousVideo.spec.ts b/tests/utilities/encodedVideoStorage/deletePreviousVideo.spec.ts new file mode 100644 index 0000000000..ae3abe14ce --- /dev/null +++ b/tests/utilities/encodedVideoStorage/deletePreviousVideo.spec.ts @@ -0,0 +1,45 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { deletePreviousVideo } from "../../../src/utilities/encodedVideoStorage/deletePreviousVideo"; +import { EncodedVideo } from "../../../src/models/EncodedVideo"; +import { uploadEncodedVideo } from "../../../src/utilities/encodedVideoStorage/uploadEncodedVideo"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testPreviousVideoPath: string; +const vid = + "data:video/mp4;base64,AAAAGGZ0eXBtcDQyAAAAAG1wNzEAAAABBGFtYXB0aW9uICogAAAAHE1vYmlsZSBsaW5lIHZpYSBteXNxbCBkZyBtZWV0IHVwbG9hZGVkIGJ5IHNwZWNpZmllZC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byB0aGUgZGF0YS4gQnkgdGhvdWdodCB0aGVpciBlbmNvZGluZyBhcHBsaWNhdGlvbnMgaW5jbHVkaW5nIGVtYWlsIHZpYSBNSU1FLCBsaWtlIHZlcnkgY29tcGxleCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byBlbnN1cmUgdGhhdCB0aGVyZSBpcyBhIG5lZWQgdG8gYmUgc3RvcnJlZCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byBlbnN1cmUgdGhhdCB0aGVyZSBpcyBhIG5lZWQgdG8gYmUgc3RvcnJlZCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byBlbnN1cmUgdGhhdCB0aGVyZSBpcyBhIG5lZWQgdG8gYmUgc3RvcnJlZCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byBlbnN1cmUgdGhhdCB0aGVyZSBpcyBhIG5lZWQgdG8gYmUgc3RvcnJlZCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byBlbnN1cmUgdGhhdCB0aGVyZSBpcyBhIG5lZWQgdG8gYmUgc3RvcnJlZCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byBlbnN1cmUgdGhhdCB0aGVyZSBpcyBhIG5lZWQgdG8gYmUgc3RvcnJlZCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byBlbnN1cmUgdGhhdCB0aGVyZSBpcyBhIG5lZWQgdG8gYmUgc3RvcnJlZCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byBlbnN1cmUgdGhhdCB0aGVyZSBpcyBhIG5lZWQgdG8gYmUgc3RvcnJlZCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4gVGhpcyBlbmNvZGluZyBoZWxwcyB0byBlbnN1cmUgdGhhdCB0aGVyZSBpcyBhIG5lZWQgdG8gYmUgc3RvcnJlZCBkYXRhLCBlc3BlY2lhbGx5IHdoZW4gdGhhdCBldmVyeSBkYXRhIG5lZWRzIHRvIGJlIHN0b3JlZCBhbmQgdHJhbnNmZXJyZWQgb3ZlciBtZWRpYSB0aGF0IGFyZSBkZXNpZ25lZCB0byBkZWFsIHdpdGggdGV4dC4="; + +// Replace with your actual base64-encoded video data + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + await EncodedVideo.deleteMany({}); + testPreviousVideoPath = await uploadEncodedVideo(vid, null); // Upload video instead of image + await uploadEncodedVideo(vid, null); // Upload another video +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("src -> utilities -> encodedVideoStorage -> deletePreviousVideo", () => { + it("should not delete the video from the FileSystem but decrement the number of uses", async () => { + const encodedVideoBefore = await EncodedVideo.findOne({ + fileName: testPreviousVideoPath, + }); + + expect(encodedVideoBefore?.numberOfUses).toBe(2); + + await deletePreviousVideo(testPreviousVideoPath); + + const encodedVideoAfter = await EncodedVideo.findOne({ + fileName: testPreviousVideoPath, + }); + expect(encodedVideoAfter?.numberOfUses).toBe(1); + }); + + it("should delete the video from the filesystem if the number of uses is only one at that point", async () => { + await deletePreviousVideo(testPreviousVideoPath); + }); +}); diff --git a/tests/utilities/encodedVideoStorage/encodedVideoExtensionCheck.spec.ts b/tests/utilities/encodedVideoStorage/encodedVideoExtensionCheck.spec.ts new file mode 100644 index 0000000000..499e1a205b --- /dev/null +++ b/tests/utilities/encodedVideoStorage/encodedVideoExtensionCheck.spec.ts @@ -0,0 +1,56 @@ +import "dotenv/config"; +import { describe, expect, it } from "vitest"; +import { encodedImageExtentionCheck } from "../../../src/utilities/encodedImageStorage/encodedImageExtensionCheck"; +import { encodedVideoExtentionCheck } from "../../../src/utilities/encodedVideoStorage/encodedVideoExtensionCheck"; + +describe("src -> utilities -> encodedImageStorage -> ", () => { + it("should return true when image extension = image/png", () => { + const data = "data:image/png;base64"; + + const result = encodedImageExtentionCheck(data); + + expect(result).toBe(true); + }); + + it("should return true when image extension = image/jpg", () => { + const data = "data:image/jpg;base64"; + + const result = encodedImageExtentionCheck(data); + + expect(result).toBe(true); + }); + + it("should return true when image extension = image/jpeg", () => { + const data = "data:image/jpeg;base64"; + + const result = encodedImageExtentionCheck(data); + + expect(result).toBe(true); + }); + + it("should return false when image extension = image/gif", () => { + const data = "data:image/gif;base64"; + + const result = encodedImageExtentionCheck(data); + + expect(result).toBe(false); + }); +}); + +describe("src -> utilities -> encodedVideoStorage -> ", () => { + it("should return true when video extension = video/mp4", () => { + const data = "data:video/mp4;base64"; + + const result = encodedVideoExtentionCheck(data); + + expect(result).toBe(true); + }); + + it("should return false when video extension = video/avi", () => { + const data = "data:video/avi;base64"; + + const result = encodedVideoExtentionCheck(data); + + expect(result).toBe(false); + }); +}); diff --git a/tests/utilities/encodedVideoStorage/uploadEncodedVideo.spec.ts b/tests/utilities/encodedVideoStorage/uploadEncodedVideo.spec.ts new file mode 100644 index 0000000000..bff2bb1bba --- /dev/null +++ b/tests/utilities/encodedVideoStorage/uploadEncodedVideo.spec.ts @@ -0,0 +1,75 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import type mongoose from "mongoose"; +import * as fs from "fs"; +import { uploadEncodedVideo } from "../../../src/utilities/encodedVideoStorage/uploadEncodedVideo"; // Import the video upload function +import { connect, disconnect } from "../../helpers/db"; +import path from "path"; +import { INVALID_FILE_TYPE } from "../../../src/constants"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testPreviousVideoPath: string; // Update variable name for video + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("src -> utilities -> encodedVideoStorage -> uploadEncodedVideo", () => { + // Update the description + it("should not create new video when the file extension is invalid", async () => { + const { requestContext } = await import("../../../src/libraries"); + + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + try { + const vid = "data:video/avi;base64,VIDEO_BASE64_DATA_HERE"; // Use an invalid video format + + await uploadEncodedVideo(vid, null); + } catch (error: any) { + expect(error.message).toEqual(`Translated ${INVALID_FILE_TYPE.MESSAGE}`); + expect(spy).toBeCalledWith(INVALID_FILE_TYPE.MESSAGE); + } + }); + + it("should create new video", async () => { + try { + const vid = "data:video/mp4;base64,VIDEO_BASE64_DATA_HERE"; // Replace with valid video data + const fileName = await uploadEncodedVideo(vid, null); + expect(fileName).not.toBe(null); + } catch (error: any) { + console.log(error); + } + }); + + it("should not create new video but return the pointer to that binary data", async () => { + try { + const vid = "data:video/mp4;base64,VIDEO_BASE64_DATA_HERE"; // Replace with valid video data + const fileName = await uploadEncodedVideo(vid, null); + expect(fileName).not.toBe(null); + testPreviousVideoPath = fileName; // Update variable name + } catch (error: any) { + console.log(error); + } + }); + + it("should not create new video but return the pointer to that binary data and delete the previous video", async () => { + try { + const vid = "data:video/mp4;base64,VIDEO_BASE64_DATA_HERE"; // Replace with valid video data + const fileName = await uploadEncodedVideo(vid, testPreviousVideoPath); // Update variable name + expect(fileName).not.toBe(null); + + if (fs.existsSync(path.join(__dirname, "../../../".concat(fileName)))) { + fs.unlink(path.join(__dirname, "../../../".concat(fileName)), (err) => { + if (err) throw err; + }); + } + } catch (error: any) { + console.log(error); + } + }); +}); diff --git a/videos/EvjEf36fQvideo.mp4 b/videos/EvjEf36fQvideo.mp4 new file mode 100644 index 0000000000..9706102199 Binary files /dev/null and b/videos/EvjEf36fQvideo.mp4 differ diff --git a/videos/WbatZ4vXEvideo.mp4 b/videos/WbatZ4vXEvideo.mp4 new file mode 100644 index 0000000000..7936dc0909 Binary files /dev/null and b/videos/WbatZ4vXEvideo.mp4 differ diff --git a/videos/cPs7nTIg3video.mp4 b/videos/cPs7nTIg3video.mp4 new file mode 100644 index 0000000000..c37f31e872 Binary files /dev/null and b/videos/cPs7nTIg3video.mp4 differ diff --git a/videos/demo.mp4 b/videos/demo.mp4 new file mode 100644 index 0000000000..7a68603be8 Binary files /dev/null and b/videos/demo.mp4 differ diff --git a/videos/dioITl-uzvideo.mp4 b/videos/dioITl-uzvideo.mp4 new file mode 100644 index 0000000000..3f8fe0f88e --- /dev/null +++ b/videos/dioITl-uzvideo.mp4 @@ -0,0 +1 @@ +T€Ä;ð@HN¸ü0ñÄD \ No newline at end of file diff --git a/videos/hero-video.mp4 b/videos/hero-video.mp4 new file mode 100644 index 0000000000..0e1d374499 Binary files /dev/null and b/videos/hero-video.mp4 differ