Skip to content

Commit

Permalink
#2 - WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
frederikpyt committed Sep 28, 2023
1 parent 8218f36 commit e914db0
Show file tree
Hide file tree
Showing 19 changed files with 1,083 additions and 91 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ REDIS_HOST="redis"
REDIS_PORT="6379"
REDIS_USER=""
REDIS_PASSWORD=""
REDIS_URL="rediss://${REDIS_USER}:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}"
REDIS_URL="rediss://${REDIS_USER}:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}"

# Vercel Blob Storage
BLOB_READ_WRITE_TOKEN=""
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
--health-timeout 5s
--health-retries 5
env:
BLOB_READ_WRITE_TOKEN: 'sometoken'
DATABASE_URL: 'postgresql://postgres_user:postgres_password@localhost:5432/postgres_db'
DATABASE_URL_NON_POOLING: 'postgresql://postgres_user:postgres_password@localhost:5432/postgres_db'
DATABASE_URL_WITHOUT_SCHEMA: 'postgresql://postgres_user:postgres_password@localhost:5432'
Expand Down
604 changes: 531 additions & 73 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "This project is a fastify template",
"main": "src/server.ts",
"scripts": {
"test": "jest",
"test": "jest --runInBand",
"test-fast": "jest",
"build": "rm -rf build && tsc -p tsconfig.build.json",
"vercel-build": "prisma generate && npm run build",
"start": "node build/src/server.js",
Expand All @@ -26,6 +27,7 @@
"@fastify/swagger": "^8.10.0",
"@fastify/swagger-ui": "^1.9.3",
"@prisma/client": "^5.3.1",
"@vercel/blob": "^0.12.5",
"bcrypt": "^5.1.1",
"dotenv": "^16.3.1",
"fastify": "^4.23.2",
Expand Down
35 changes: 35 additions & 0 deletions prisma/migrations/20230927083830_item/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- CreateTable
CREATE TABLE "Item" (
"id" SERIAL NOT NULL,
"name" VARCHAR(50) NOT NULL,
"mimeType" VARCHAR(255) NOT NULL,
"blobUrl" VARCHAR(1024) NOT NULL,
"ownerId" INTEGER NOT NULL,
"parentId" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),

CONSTRAINT "Item_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "Item_blobUrl_idx" ON "Item"("blobUrl");

-- CreateIndex
CREATE INDEX "Item_ownerId_idx" ON "Item"("ownerId");

-- CreateIndex
CREATE INDEX "Item_parentId_idx" ON "Item"("parentId");

-- CreateIndex
CREATE INDEX "Item_ownerId_parentId_idx" ON "Item"("ownerId", "parentId");

-- CreateIndex
CREATE INDEX "Item_deletedAt_idx" ON "Item"("deletedAt");

-- AddForeignKey
ALTER TABLE "Item" ADD CONSTRAINT "Item_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Item" ADD CONSTRAINT "Item_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Item"("id") ON DELETE SET NULL ON UPDATE CASCADE;
55 changes: 42 additions & 13 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,54 @@ datasource db {
}

model User {
id Int @id @default(autoincrement())
name String @db.VarChar(50)
email String @unique @db.VarChar(255)
password String @db.Text
sessions UserSession[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @id @default(autoincrement())
name String @db.VarChar(50)
email String @unique @db.VarChar(255)
password String @db.Text
sessions UserSession[]
Item Item[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model UserSession {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
refreshToken String @db.VarChar(1024)
tokenFamily String @db.VarChar(36)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
refreshToken String @db.VarChar(1024)
tokenFamily String @db.VarChar(36)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tokenFamily])
@@index([userId])
@@index([userId, tokenFamily])
}

model Item {
id Int @id @default(autoincrement())
name String @db.VarChar(50)
mimeType String @db.VarChar(255)
blobUrl String @db.VarChar(1024)
ownerId Int
parentId Int?
owner User @relation(fields: [ownerId], references: [id])
parentItem Item? @relation("ItemToItem", fields: [parentId], references: [id])
Items Item[] @relation("ItemToItem")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([blobUrl])
@@index([ownerId])
@@index([parentId])
@@index([ownerId, parentId])
@@index([deletedAt])
}
2 changes: 2 additions & 0 deletions src/modules/auth/__test__/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { jwt } from '../../../plugins/jwt';
import TimeUtil from '../../../utils/time';
import UserService from '../user.service';
import AuthService from '../auth.service';
import { v4 } from 'uuid';

describe('GET /api/auth/user', () => {
let userService: UserService;
Expand Down Expand Up @@ -49,6 +50,7 @@ describe('GET /api/auth/user', () => {
jwt.signAccessToken({
sub: 542,
iat: TimeUtil.getNowUnixTimeStamp(),
tokenFamily: v4(),
}),
},
});
Expand Down
6 changes: 5 additions & 1 deletion src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { FastifyInstance, FastifyPluginOptions } from 'fastify';

import fastifyPlugin from 'fastify-plugin';
import auth from './auth';
import item from './item';

const getOptionsWithPrefix = (options: FastifyPluginOptions, prefix: string) => {
return {
Expand All @@ -15,5 +16,8 @@ export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPl
return { status: 'OK' };
});

await Promise.all([fastify.register(auth, getOptionsWithPrefix(options, '/auth'))]);
await Promise.all([
fastify.register(auth, getOptionsWithPrefix(options, '/auth')),
fastify.register(item, getOptionsWithPrefix(options, '/item')),
]);
});
187 changes: 187 additions & 0 deletions src/modules/item/__test__/upload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { User } from '@prisma/client';
import UserService from '../../auth/user.service';
import AuthService from '../../auth/auth.service';

describe('POST /api/item', () => {
let userService: UserService;
let authService: AuthService;

let user: User;

beforeAll(async () => {
authService = new AuthService();
userService = new UserService();

user = await userService.createUser({
name: 'Joe Biden the 1st',
email: '[email protected]',
password: '1234',
});
});

/*
* Generate client token tests
*/

it('should return status 200 and return a new clientToken', async () => {
const { accessToken } = await authService.createTokens(user.id);

const response = await global.fastify.inject({
method: 'POST',
url: '/api/item',
headers: {
'content-type': 'text/plain',
authorization: 'Bearer ' + accessToken,
},
payload: {
type: 'blob.generate-client-token',
payload: {
callbackUrl: 'https://example.com/api/item',
clientPayload: JSON.stringify({ parentId: null }),
pathname: 'test.txt',
},
},
});

expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
type: 'blob.generate-client-token',
clientToken: expect.any(String),
});
});

it('should return status 401, when unauthorized', async () => {
const response = await global.fastify.inject({
method: 'POST',
url: '/api/item',
headers: {
'content-type': 'text/plain',
authorization: 'invalid_access_token!!!',
},
payload: {
type: 'blob.generate-client-token',
payload: {
callbackUrl: 'https://example.com/api/item',
clientPayload: JSON.stringify({ parentId: null }),
pathname: 'test.txt',
},
},
});

expect(response.statusCode).toBe(401);
expect(response.json()).toEqual({
error: 'Unauthorized',
message: 'Unauthorized',
statusCode: 401,
});
});

it('should return status 400, when clientPayload is missing', async () => {
const { accessToken } = await authService.createTokens(user.id);

const response = await global.fastify.inject({
method: 'POST',
url: '/api/item',
headers: {
'content-type': 'text/plain',
authorization: 'Bearer ' + accessToken,
},
payload: {
type: 'blob.generate-client-token',
payload: {
callbackUrl: 'https://example.com/api/item',
pathname: 'test.txt',
},
},
});

expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({
error: 'Bad Request',
message: 'No clientPayload was passed!',
statusCode: 400,
});
});

it('should return status 400, when clientPayload.parentId is missing', async () => {
const { accessToken } = await authService.createTokens(user.id);

const response = await global.fastify.inject({
method: 'POST',
url: '/api/item',
headers: {
'content-type': 'text/plain',
authorization: 'Bearer ' + accessToken,
},
payload: {
type: 'blob.generate-client-token',
payload: {
callbackUrl: 'https://example.com/api/item',
clientPayload: JSON.stringify({}),
pathname: 'test.txt',
},
},
});

expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({
error: 'Bad Request',
message: 'clientPayload.parentId is required!',
statusCode: 400,
});
});

it('should return status 400, when providing invalid json string as payload', async () => {
const { accessToken } = await authService.createTokens(user.id);

const response = await global.fastify.inject({
method: 'POST',
url: '/api/item',
headers: {
'content-type': 'text/plain',
authorization: 'Bearer ' + accessToken,
},
payload: 'Invalid json',
});

expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({
error: 'Bad Request',
message: expect.stringContaining('Unexpected token'),
statusCode: 400,
});
});

/*
* Upload completed callback tests
*/

it('should return status 400, when called without valid "x-vercel-signature" header', async () => {
const response = await global.fastify.inject({
method: 'POST',
url: '/api/item',
headers: {
'content-type': 'text/plain',
},
payload: {
type: 'blob.upload-completed',
payload: {
blob: {
url: 'https://example.com/test-ihufsdihudsfuds.txt',
pathname: 'test.txt',
contentType: 'text/plain',
contentDisposition: 'attachment; filename="test.txt"',
},
tokenPayload: JSON.stringify({ parentId: null, ownerId: user.id }),
},
},
});

expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({
error: 'Bad Request',
message: 'Vercel Blob: Missing callback signature',
statusCode: 400,
});
});
});
Loading

0 comments on commit e914db0

Please sign in to comment.