v1.0.0 #28
|
@ -0,0 +1,22 @@
|
||||||
|
name: Unit Tests with docker compose
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit-test:
|
||||||
|
container:
|
||||||
|
image: docker:dind
|
||||||
|
volumes:
|
||||||
|
- /usr/local/share/.cache/yarn:/usr/local/share/.cache/yarn
|
||||||
|
- /var/lib/docker/image:/var/lib/docker/image
|
||||||
|
- /var/lib/docker/overlay2:/var/lib/docker/overlay2
|
||||||
|
steps:
|
||||||
|
- name: Starting docker daemon
|
||||||
|
run: docker-init -- dockerd --host=unix:///var/run/docker.sock &
|
||||||
|
- name: Installing necessary packages
|
||||||
|
run: apk add yarn git curl bash
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install project dependencies
|
||||||
|
run: yarn install --frozen-lockfile --ignore-scripts
|
||||||
|
- name: Run docker-compose
|
||||||
|
run: docker compose down -v && docker compose run bot-api bun test
|
|
@ -0,0 +1,52 @@
|
||||||
|
name: Build image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- build
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
container:
|
||||||
|
image: docker:dind
|
||||||
|
volumes:
|
||||||
|
- /data/.cache/act:/data/.cache/act
|
||||||
|
- /var/lib/docker/image:/var/lib/docker/image
|
||||||
|
- /var/lib/docker/overlay2:/var/lib/docker/overlay2
|
||||||
|
steps:
|
||||||
|
- name: Starting docker daemon
|
||||||
|
run: docker-init -- dockerd --host=unix:///var/run/docker.sock &
|
||||||
|
- name: Installing necessary packages
|
||||||
|
run: apk add nodejs git curl bash
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: https://github.com/docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
# list of Docker images to use as base name for tags
|
||||||
|
images: |
|
||||||
|
git.fai.st/fedi-image-bot/bot-api
|
||||||
|
# generate Docker tags based on the following events/attributes
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{raw}}
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to fai.st docker registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: git.fai.st
|
||||||
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_PASS }}
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
|
@ -1,2 +1,7 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
.vscode
|
||||||
|
.env
|
||||||
|
.editorconfig
|
||||||
|
.idea
|
||||||
|
coverage*
|
||||||
|
|
32
Dockerfile
32
Dockerfile
|
@ -5,36 +5,28 @@
|
||||||
FROM oven/bun:1 as base
|
FROM oven/bun:1 as base
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Generate new bun lock
|
||||||
|
FROM base as lock
|
||||||
|
RUN mkdir -p /temp/lock
|
||||||
|
COPY package.json /temp/lock
|
||||||
|
RUN cd /temp/lock && bun install
|
||||||
|
|
||||||
# install dependencies into temp folder
|
# install dependencies into temp folder
|
||||||
# this will cache them and speed up future builds
|
# this will cache them and speed up future builds
|
||||||
FROM base AS install
|
FROM base AS install
|
||||||
RUN mkdir -p /temp/dev
|
|
||||||
COPY package.json bun.lockb /temp/dev/
|
|
||||||
RUN cd /temp/dev && bun install
|
|
||||||
|
|
||||||
# install with --production (exclude devDependencies)
|
# install with --production (exclude devDependencies)
|
||||||
RUN mkdir -p /temp/prod
|
RUN mkdir -p /temp/prod
|
||||||
COPY package.json bun.lockb /temp/prod/
|
COPY --from=lock /temp/lock/package.json /temp/lock/bun.lockb /temp/prod/
|
||||||
RUN cd /temp/prod && bun install --production
|
RUN cd /temp/prod && bun install --production --frozen-lockfile
|
||||||
|
|
||||||
# copy node_modules from temp folder
|
# Copy production dependencies and source code into final image
|
||||||
# then copy all (non-ignored) project files into the image
|
|
||||||
FROM install AS prerelease
|
|
||||||
COPY --from=install /temp/dev/node_modules node_modules
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# [optional] tests & build
|
|
||||||
# ENV NODE_ENV=production
|
|
||||||
# RUN bun test
|
|
||||||
# RUN bun run build
|
|
||||||
|
|
||||||
# copy production dependencies and source code into final image
|
|
||||||
FROM base AS release
|
FROM base AS release
|
||||||
COPY --from=install /temp/prod/node_modules node_modules
|
COPY --from=install /temp/prod/node_modules node_modules
|
||||||
COPY --from=prerelease /usr/src/app/index.ts .
|
COPY --from=install /temp/prod/package.json .
|
||||||
COPY --from=prerelease /usr/src/app/package.json .
|
COPY ./src ./src
|
||||||
|
|
||||||
# run the app
|
# run the app
|
||||||
USER bun
|
USER bun
|
||||||
|
ENV NODE_ENV production
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
CMD ["bun", "run", "start"]
|
CMD ["bun", "run", "start"]
|
||||||
|
|
53
README.md
53
README.md
|
@ -1,5 +1,52 @@
|
||||||
# bot-api
|
# bot-api
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
The function of the API is basically to access the images' metadata stored in the database.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
The API exposes some endpoints to interact with the database.
|
||||||
|
|
||||||
|
### GET `/images`
|
||||||
|
Allows to get a list of image documents.
|
||||||
|
|
||||||
|
#### Query params
|
||||||
|
- `limit`: an optional parameter, which accepts a non-negative integer that dictates the number of documents that the list will have. If its value is equal to `0`, or if this parameter is missing, the endpoint will return all the image documents in the database.
|
||||||
|
- `status`: an optional parameter, which accepts the values `consumed`, `available` and `unavailable`. It filters the documents that have only the `status` attribute equal to that indicated in the parameter's value. If the parameter is missing, no filter will be applied to the document.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
- `GET /images?limit=5&status=available` will return 5 documents that have the `available` value in their `status` attribute.
|
||||||
|
|
||||||
|
### GET `/images/<id>`
|
||||||
|
Allows to get an image document.
|
||||||
|
|
||||||
|
#### Params
|
||||||
|
- `id`: the id of the document to be modified.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
`GET /images/61f7e48f0c651345677b7775` will get the document referenced by the `id` param.
|
||||||
|
|
||||||
|
### PUT `/images/<id>`
|
||||||
|
Modifies an existing image document. The request must provide a JSON-formatted body, with one or more valid document attributes. The existing document attributes will be replaced with the provided new ones.
|
||||||
|
|
||||||
|
#### Params
|
||||||
|
- `id`: the id of the document to be modified.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
- `PUT /images/61f7e48f0c651345677b7775` with body `{ "status": "consumed" }` will modify the document referenced by the `id` param, changing their `status` value to `consumed`.
|
||||||
|
|
||||||
|
### POST `/images`
|
||||||
|
Allows to insert a new image document.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
`POST /images` with body `{ "url": "https://my-images.com/foo.jpg", "status": "available", "tags": ["foo", "bar"] }` will insert the image passed on the request body into the database.
|
||||||
|
|
||||||
|
### POST `/login`
|
||||||
|
Generates an access token to use in future requests to authenticated endpoints if a valid App and Secret are provided.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
`POST /login` with body `{ "app": "example", "secret": "badsecret" }` will return `{ "token": "x.y.z" }` "x.y.z" being a JWT token to use on further requests if the App and Secret are valid.
|
||||||
|
|
||||||
|
## Installation
|
||||||
To install dependencies:
|
To install dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -9,13 +56,13 @@ bun install
|
||||||
To run:
|
To run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
For testing, remember:
|
For testing, remember:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose down -v
|
bun run test
|
||||||
docker compose run bot-api bun run test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This project was created using `bun init` in bun v1.0.13. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
This project was created using `bun init` in bun v1.0.13. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
[install.lockfile]
|
||||||
|
# whether to save a non-Bun lockfile alongside bun.lockb
|
||||||
|
# only "yarn" is supported
|
||||||
|
print = "yarn"
|
|
@ -1,8 +1,8 @@
|
||||||
version: '3'
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mongodb:
|
mongodb:
|
||||||
image: mongo
|
image: mongo:bionic
|
||||||
container_name: mongodb
|
container_name: mongodb
|
||||||
ports:
|
ports:
|
||||||
- "27017:27017"
|
- "27017:27017"
|
||||||
|
@ -12,7 +12,6 @@ services:
|
||||||
MONGO_INITDB_DATABASE: bot
|
MONGO_INITDB_DATABASE: bot
|
||||||
volumes:
|
volumes:
|
||||||
- mongodb_data:/data/db
|
- mongodb_data:/data/db
|
||||||
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
|
|
||||||
|
|
||||||
bot-api:
|
bot-api:
|
||||||
image: oven/bun:1
|
image: oven/bun:1
|
||||||
|
@ -27,6 +26,7 @@ services:
|
||||||
MONGODB_URI: "mongodb://mongodb:27017/bot"
|
MONGODB_URI: "mongodb://mongodb:27017/bot"
|
||||||
MONGODB_USER: "root"
|
MONGODB_USER: "root"
|
||||||
MONGODB_PASS: "password"
|
MONGODB_PASS: "password"
|
||||||
|
JWTSECRET: "cooljwtsecret"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/usr/src/app:ro
|
- ./:/usr/src/app:ro
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
db.createUser({
|
|
||||||
user: 'root',
|
|
||||||
pwd: 'password',
|
|
||||||
roles: [
|
|
||||||
{
|
|
||||||
role: 'readWrite',
|
|
||||||
db: 'admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'readWrite',
|
|
||||||
db: 'bot',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
db = new Mongo().getDB("bot");
|
|
||||||
|
|
||||||
db.images.createIndex({ "status": 1 });
|
|
||||||
db.images.createIndex({ "url": 1 }, { "unique": true });
|
|
||||||
db.images.insert({
|
|
||||||
url: "https://example.com",
|
|
||||||
status: "consumed",
|
|
||||||
tags: ["2girls", "sleeping"]
|
|
||||||
});
|
|
16
package.json
16
package.json
|
@ -1,13 +1,19 @@
|
||||||
{
|
{
|
||||||
"name": "fib-api",
|
"name": "bot-api",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^8.3.1",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/express-list-endpoints": "^6.0.3",
|
||||||
"@types/jest": "^29.5.11",
|
"@types/jest": "^29.5.11",
|
||||||
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/supertest": "^6.0.1",
|
"@types/supertest": "^6.0.1",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
|
"enforce-unique": "^1.2.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
"ts-jest": "^29.1.1"
|
"ts-jest": "^29.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -20,9 +26,11 @@
|
||||||
"test": "docker compose down -v && docker compose run bot-api bun test"
|
"test": "docker compose down -v && docker compose run bot-api bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/express": "^4.17.21",
|
"compression": "^1.7.4",
|
||||||
"@types/mongoose": "^5.11.97",
|
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"mongoose": "^8.0.3"
|
"express-list-endpoints": "^6.0.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mongoose": "^8.0.3",
|
||||||
|
"winston": "^3.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
import mongoose from "mongoose";
|
|
||||||
|
|
||||||
const ImageSchema = new mongoose.Schema({
|
|
||||||
url: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: String,
|
|
||||||
enum: {
|
|
||||||
values: ["consumed", "unavailable", "available"],
|
|
||||||
},
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
type: [String]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default mongoose.model('images', ImageSchema);
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import express from "express";
|
||||||
|
import listEndpoints from "express-list-endpoints";
|
||||||
|
import imageController from "./controllers/ImageController";
|
||||||
|
import authControler from "./controllers/AuthControler";
|
||||||
|
import mongoose from "mongoose";
|
||||||
|
import compression from "compression";
|
||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
|
export const app = express();
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(compression());
|
||||||
|
|
||||||
|
app.get("/", (_, res) => {
|
||||||
|
const endpoints = listEndpoints(app);
|
||||||
|
res.json({ endpoints });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/images", imageController.getAllImages);
|
||||||
|
app.get("/images/:id", imageController.getImageById);
|
||||||
|
app.put("/images/:id", authControler.authorize, imageController.editImage);
|
||||||
|
app.post("/images", authControler.authorize, imageController.addImage);
|
||||||
|
app.post("/login", authControler.login);
|
||||||
|
|
||||||
|
export const startApp = async () => {
|
||||||
|
const port = process.env.PORT || 8080;
|
||||||
|
const mongo_uri: string = process.env.MONGODB_URI || "";
|
||||||
|
const mongo_user = process.env.MONGODB_USER;
|
||||||
|
const mongo_pass = process.env.MONGODB_PASS;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mongoose.connect(mongo_uri, {
|
||||||
|
authSource: "admin",
|
||||||
|
user: mongo_user,
|
||||||
|
pass: mongo_pass,
|
||||||
|
});
|
||||||
|
mongoose.set("runValidators", true);
|
||||||
|
app.listen(port, () =>
|
||||||
|
logger.info(`Express server listening on port ${port}`)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default app;
|
|
@ -0,0 +1,52 @@
|
||||||
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
import AuthService from "../services/AuthService";
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
|
const authTokenSecret = process.env.JWTSECRET || "badsecret";
|
||||||
|
|
||||||
|
class AuthControler {
|
||||||
|
async login(req: Request, res: Response) {
|
||||||
|
// Read app and secret from request body
|
||||||
|
const { app, secret } = req.body;
|
||||||
|
|
||||||
|
// Filter app from the apps by app and secret
|
||||||
|
const authenticated = await AuthService.find(app, secret);
|
||||||
|
|
||||||
|
if (authenticated) {
|
||||||
|
logger.info("Authenticated app", authenticated.app);
|
||||||
|
// Generate an access token
|
||||||
|
const token = jwt.sign({ app: authenticated.app }, authTokenSecret, {
|
||||||
|
expiresIn: "1h",
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ token });
|
||||||
|
} else {
|
||||||
|
logger.warn("Authentication attempt for", app);
|
||||||
|
res.status(403).send("Credentials incorrect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authorize(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (authHeader) {
|
||||||
|
const token = authHeader.split(" ")[1];
|
||||||
|
|
||||||
|
jwt.verify(token, authTokenSecret, (err, app) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(403).json("Invalid token provided");
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
`Authorization for ${req.method} ${req.url} provided to app ${
|
||||||
|
(app as JwtPayload).app
|
||||||
|
} `
|
||||||
|
);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(401).json("No Authorization header provided");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AuthControler();
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import imageService from "../services/ImageService";
|
||||||
|
import mongoose, { mongo } from "mongoose";
|
||||||
|
import { Image } from "../models/ImageModel";
|
||||||
|
|
||||||
|
class ImageController {
|
||||||
|
async editImage(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const change: Image = req.body;
|
||||||
|
const result = await imageService.replaceOne(req.params.id, change);
|
||||||
|
if (result.matchedCount > 0) {
|
||||||
|
res.status(204).json();
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: "Image not found" });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof mongoose.Error.CastError) {
|
||||||
|
res.status(400).json({ error: "Invalid Id" });
|
||||||
|
} else if (error instanceof mongoose.Error.ValidationError) {
|
||||||
|
// Should return 400 Bad request for invalid requests
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
} else if (
|
||||||
|
error instanceof mongo.MongoServerError &&
|
||||||
|
error.code === 11000
|
||||||
|
) {
|
||||||
|
// Should return 409 Conflict for existing urls
|
||||||
|
res.status(409).json({
|
||||||
|
error: `the image with URL ${error.keyValue.url} already exists`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getImageById(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const image = await imageService.findById(req.params.id);
|
||||||
|
if (image) {
|
||||||
|
res.json({ image });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: "Image not found" });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof mongoose.Error.CastError) {
|
||||||
|
res.status(400).json({ error: "Invalid Id" });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllImages(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
req.query.status !== undefined &&
|
||||||
|
!["consumed", "unavailable", "available"].includes(
|
||||||
|
req.query.status as string
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw TypeError(
|
||||||
|
"if present, `status` should have the values `consumed`, `unavailable`, or `available`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const limit = req.query.limit ? Number(req.query.limit) : undefined;
|
||||||
|
const status = req.query.status
|
||||||
|
? (req.query.status as Image["status"])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const images = await imageService.findAll(limit, status);
|
||||||
|
res.json({ images });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TypeError) {
|
||||||
|
res.status(400).json({ error });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addImage(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Should add auth here before doing stuff
|
||||||
|
// Thowing a 401 if not auth provided
|
||||||
|
// Throwing a 403 for incorrect auth
|
||||||
|
const image = await imageService.add(req.body);
|
||||||
|
res.status(201).json({ image });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof mongo.MongoServerError && error.code === 11000) {
|
||||||
|
// Should return 409 Conflict for existing urls
|
||||||
|
res.status(409).json({
|
||||||
|
error: `the image with URL ${error.keyValue.url} already exists`,
|
||||||
|
});
|
||||||
|
} else if (error instanceof mongoose.Error.ValidationError) {
|
||||||
|
// Should return 400 Bad request for invalid requests
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
} else {
|
||||||
|
// Return 500 in other case
|
||||||
|
res.status(500).json({ error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ImageController();
|
67
src/index.ts
67
src/index.ts
|
@ -1,61 +1,12 @@
|
||||||
import express from "express";
|
import { startApp } from "./app";
|
||||||
import mongoose from "mongoose";
|
|
||||||
import ImageModel from "./ImageModel";
|
|
||||||
|
|
||||||
export const app = express();
|
await startApp();
|
||||||
|
|
||||||
app.use(express.json());
|
// This try carch is to prevent hot reload from making the process die due to coliding entries
|
||||||
|
try {
|
||||||
app.get("/", (_, res) => {
|
// Not insert test data into production
|
||||||
res.json({ message: "Blazing fast 🚀" });
|
if (process.env.NODE_ENV != "production") {
|
||||||
});
|
const populateDatabase = require("../tests/populateDatabase");
|
||||||
|
await populateDatabase();
|
||||||
app.get("/images", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const allImages = await ImageModel.find();
|
|
||||||
res.json(allImages);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ message: error });
|
|
||||||
}
|
}
|
||||||
})
|
} catch {}
|
||||||
|
|
||||||
app.post("/images", async (req, res) => {
|
|
||||||
try {
|
|
||||||
// Should add auth here before doing stuff
|
|
||||||
// Thowing a 401 if not auth provided
|
|
||||||
// Throwing a 403 for incorrect auth
|
|
||||||
const image = await ImageModel.create(req.body);
|
|
||||||
res.status(201).json(image);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code == 11000){
|
|
||||||
// Should return 409 Conflict for existing urls
|
|
||||||
res.status(409).json({ message: "Existing URL" });
|
|
||||||
}
|
|
||||||
// Should return 400 Bad request for invalid requests
|
|
||||||
res.status(400).json({ message: error });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set the default port to 8080, or use the PORT environment variable
|
|
||||||
|
|
||||||
const start = async () => {
|
|
||||||
const port = process.env.PORT || 8080;
|
|
||||||
const mongo_uri: string = process.env.MONGODB_URI || "";
|
|
||||||
const mongo_user = process.env.MONGODB_USER;
|
|
||||||
const mongo_pass = process.env.MONGODB_PASS;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mongoose.connect(mongo_uri, {
|
|
||||||
authSource: "admin",
|
|
||||||
user: mongo_user,
|
|
||||||
pass: mongo_pass
|
|
||||||
});
|
|
||||||
app.listen(port, () => console.log(`Express server listening on port ${port}`));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
start();
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import winston, { format } from "winston";
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
format: format.combine(
|
||||||
|
format.timestamp(),
|
||||||
|
format.align(),
|
||||||
|
format.colorize({ all: true }),
|
||||||
|
format.printf((info) => `${info.timestamp} ${info.level}:${info.message}`)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = new winston.transports.File({ filename: "/tmp/express.log" });
|
||||||
|
const console = new winston.transports.Console();
|
||||||
|
|
||||||
|
logger.clear().add(files).add(console);
|
||||||
|
|
||||||
|
export default logger;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import mongoose, { Document } from "mongoose";
|
||||||
|
|
||||||
|
export interface Auth extends Document {
|
||||||
|
app: String;
|
||||||
|
secret: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
app: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ collection: "authorizations" }
|
||||||
|
);
|
||||||
|
|
||||||
|
export default mongoose.model("authorizations", AuthSchema);
|
|
@ -0,0 +1,32 @@
|
||||||
|
import mongoose, { Document } from "mongoose";
|
||||||
|
|
||||||
|
export interface Image extends Document {
|
||||||
|
url: string;
|
||||||
|
status: "consumed" | "unavailable" | "available";
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: {
|
||||||
|
values: ["consumed", "unavailable", "available"],
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: [String],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ collection: "images" }
|
||||||
|
);
|
||||||
|
|
||||||
|
export default mongoose.model<Image>("images", ImageSchema);
|
|
@ -0,0 +1,10 @@
|
||||||
|
import AuthModel, { Auth } from "../models/AuthModel";
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
async find(app: String, secret: String): Promise<Auth | null> {
|
||||||
|
const auth = await AuthModel.findOne({ app: app, secret: secret });
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AuthService();
|
|
@ -0,0 +1,37 @@
|
||||||
|
import imageModel, { Image } from "../models/ImageModel";
|
||||||
|
|
||||||
|
class ImageService {
|
||||||
|
async replaceOne(id: string, newimage: Image) {
|
||||||
|
const result = await imageModel.updateOne({ _id: id }, newimage);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async findById(id: string) {
|
||||||
|
const image = await imageModel.findOne({ _id: id });
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(limit?: number, status?: Image["status"]): Promise<Image[]> {
|
||||||
|
const typeError = TypeError(
|
||||||
|
"if present, `limit` must be a non-negative integer"
|
||||||
|
);
|
||||||
|
const filter = status !== undefined ? { status } : {};
|
||||||
|
let query = imageModel.find(filter);
|
||||||
|
if (limit !== undefined) {
|
||||||
|
if (Number.isInteger(limit)) {
|
||||||
|
if (limit > 0) query = query.limit(limit);
|
||||||
|
else if (limit < 0) throw typeError;
|
||||||
|
} else {
|
||||||
|
throw typeError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const allImages = await query;
|
||||||
|
return allImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(image: Image): Promise<Image> {
|
||||||
|
const newImage = await imageModel.create(image);
|
||||||
|
return newImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ImageService();
|
|
@ -1,51 +1,382 @@
|
||||||
import { beforeAll, describe, expect, it } from "bun:test";
|
import { afterEach, beforeAll, describe, expect, it, mock } from "bun:test";
|
||||||
import request from "supertest";
|
import request, { Response } from "supertest";
|
||||||
import { app } from "../src";
|
import app, { startApp } from "../src/app";
|
||||||
|
import imageService from "../src/services/ImageService";
|
||||||
|
import populateDatabase from "./populateDatabase";
|
||||||
|
import { Image } from "../src/models/ImageModel";
|
||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
|
||||||
|
const imageServiceOriginal = imageService;
|
||||||
|
|
||||||
|
let token: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await startApp();
|
||||||
|
await populateDatabase();
|
||||||
|
|
||||||
|
const tok = await request(app)
|
||||||
|
.post("/login")
|
||||||
|
.send({ app: "tester", secret: "test" });
|
||||||
|
token = tok.body.token;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
mock.module("../src/services/ImageService", () => ({
|
||||||
|
default: imageServiceOriginal,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("/login works as intended", async () => {
|
||||||
|
let correctRespose: Response;
|
||||||
|
beforeAll(async () => {
|
||||||
|
correctRespose = await request(app)
|
||||||
|
.post("/login")
|
||||||
|
.send({ app: "tester", secret: "test" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 200 for correct login", async () => {
|
||||||
|
expect(correctRespose.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain a token", () => {
|
||||||
|
expect(correctRespose.body).toHaveProperty("token");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 403 for invalid credentials", async () => {
|
||||||
|
const res = await request(app).post("/login").send({});
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET / shows all of the endpoints", async () => {
|
||||||
|
const res = await request(app).get("/");
|
||||||
|
|
||||||
|
it("should have the endpoints in the `endpoints` property, as an array", async () => {
|
||||||
|
expect(res.body).toHaveProperty("endpoints");
|
||||||
|
expect(Array.isArray(res.body.endpoints)).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("GET /images works properly", async () => {
|
describe("GET /images works properly", async () => {
|
||||||
const res = await request(app).get("/images");
|
const res = await request(app).get("/images");
|
||||||
|
|
||||||
it("should be an array", async () => {
|
it("should be an array", () => {
|
||||||
expect(Array.isArray(res.body)).toBeTrue();
|
expect(Array.isArray(res.body.images)).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a 200", async () => {
|
it("should return a 200", async () => {
|
||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return 500 for an error on the service", async () => {
|
||||||
|
mock.module("../src/services/ImageService", () => ({
|
||||||
|
default: {
|
||||||
|
findAll: () => {
|
||||||
|
throw new Error("This is an expected testing error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const res = await request(app).get("/images");
|
||||||
|
|
||||||
|
expect(res.status).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 400 for an invalid status param value", async () => {
|
||||||
|
const res = await request(app).get("/images?status=foo");
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 200 for a request with valid status param", async () => {
|
||||||
|
const status = faker.helpers.arrayElement([
|
||||||
|
"consumed",
|
||||||
|
"available",
|
||||||
|
"unavailable",
|
||||||
|
]);
|
||||||
|
const res = await request(app).get(`/images?status=${status}`);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only have the requested status in the images", async () => {
|
||||||
|
const status = faker.helpers.arrayElement([
|
||||||
|
"consumed",
|
||||||
|
"available",
|
||||||
|
"unavailable",
|
||||||
|
]);
|
||||||
|
const res = await request(app).get(`/images?status=${status}`);
|
||||||
|
expect(
|
||||||
|
res.body.images
|
||||||
|
.map((image: Image) => image.status)
|
||||||
|
.every((imageStatus: string) => imageStatus === status)
|
||||||
|
).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 400 for a floating point value in limit value", async () => {
|
||||||
|
const res = await request(app).get("/images?limit=2.4");
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 400 for a negative value in limit value", async () => {
|
||||||
|
const res = await request(app).get("/images?limit=-1");
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 400 for a NaN limit value", async () => {
|
||||||
|
const res = await request(app).get("/images?limit=foo");
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 200 for a request with valid limit param", async () => {
|
||||||
|
const limit = faker.number.int({ min: 5, max: 50 });
|
||||||
|
const res = await request(app).get(`/images?limit=${limit}`);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 200 for a request with valid limit param", async () => {
|
||||||
|
const limit = faker.number.int({ min: 5, max: 50 });
|
||||||
|
const res = await request(app).get(`/images?limit=${limit}`);
|
||||||
|
expect(res.body.images.length).toBeLessThanOrEqual(limit);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 200 for a request with both valid params", async () => {
|
||||||
|
const res = await request(app).get("/images?limit=3&status=available");
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the same ids using limit=0 and using no limit parameter", async () => {
|
||||||
|
const resLimit0 = await request(app).get("/images?limit=0");
|
||||||
|
const resNoLimit = await request(app).get("/images");
|
||||||
|
|
||||||
|
const ids1 = resNoLimit.body.images.map((image: Image) => image._id);
|
||||||
|
const ids2 = resLimit0.body.images.map((image: Image) => image._id);
|
||||||
|
|
||||||
|
expect(ids1.length).toBe(ids2.length);
|
||||||
|
ids1.forEach((id: string) => expect(ids2).toContain(id));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("POST /images works properly", () => {
|
describe("POST /images works properly", () => {
|
||||||
|
it("should return 401 for unauthenticated requests", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/images")
|
||||||
|
.send({
|
||||||
|
url: "https://test.url.com/0",
|
||||||
|
status: "available",
|
||||||
|
tags: ["2girls", "touhou"],
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 403 for invalid tokens", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/images")
|
||||||
|
.set("authorization", `Bearer token`)
|
||||||
|
.send({
|
||||||
|
url: "https://test.url.com/0",
|
||||||
|
status: "available",
|
||||||
|
tags: ["2girls", "touhou"],
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
it("should return 201 for new image", async () => {
|
it("should return 201 for new image", async () => {
|
||||||
const res = await request(app).post("/images").send({
|
const res = await request(app)
|
||||||
|
.post("/images")
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({
|
||||||
url: "https://test.url.com/1",
|
url: "https://test.url.com/1",
|
||||||
status: "available",
|
status: "available",
|
||||||
tags: ["2girls", "touhou"]
|
tags: ["2girls", "touhou"],
|
||||||
});
|
});
|
||||||
expect(res.status).toSatisfy(status => [201].includes(status));
|
expect(res.status).toBe(201);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 409 for a repeated images", async () => {
|
it("should return 409 for a repeated images", async () => {
|
||||||
await request(app).post("/images").send({
|
await request(app)
|
||||||
|
.post("/images")
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({
|
||||||
url: "https://test.url.com/2",
|
url: "https://test.url.com/2",
|
||||||
status: "available",
|
status: "available",
|
||||||
tags: ["2girls", "touhou"]
|
tags: ["2girls", "touhou"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await request(app).post("/images").send({
|
const res = await request(app)
|
||||||
|
.post("/images")
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({
|
||||||
url: "https://test.url.com/2",
|
url: "https://test.url.com/2",
|
||||||
status: "available",
|
status: "available",
|
||||||
tags: ["2girls", "touhou"]
|
tags: ["2girls", "touhou"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toSatisfy(status => [409].includes(status));
|
expect(res.status).toBe(409);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 500 for an error on the service", async () => {
|
||||||
|
mock.module("../src/services/ImageService", () => ({
|
||||||
|
default: {
|
||||||
|
add: () => {
|
||||||
|
throw new Error("This is an expected testing error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/images")
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({
|
||||||
|
url: "https://test.url.com/3",
|
||||||
|
status: "available",
|
||||||
|
tags: ["2girls", "touhou"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 400 for malformed requests", async () => {
|
it("should return 400 for malformed requests", async () => {
|
||||||
const res = await request(app).post("/images").send({
|
const res = await request(app)
|
||||||
url: "https://test.url.com/3",
|
.post("/images")
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({
|
||||||
|
url: "https://test.url.com/4",
|
||||||
status: "wrong",
|
status: "wrong",
|
||||||
tags: ["2girls", "touhou"]
|
tags: ["2girls", "touhou"],
|
||||||
});
|
});
|
||||||
expect(res.status).toSatisfy(status => [400].includes(status));
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /images/<id> works properly", () => {
|
||||||
|
let id: string;
|
||||||
|
let list: Image[];
|
||||||
|
beforeAll(async () => {
|
||||||
|
const res = await request(app).get("/images");
|
||||||
|
list = res.body.images;
|
||||||
|
id = list[0]._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 200 for existing ids", async () => {
|
||||||
|
const res = await request(app).get(`/images/${id}`);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 404 for non-existing ids", async () => {
|
||||||
|
const id = "000000000000000000000000"; // this was the least posible to exist ID
|
||||||
|
if (!(id in list)) {
|
||||||
|
const res = await request(app).get(`/images/${id}`);
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 400 for malformed ids", async () => {
|
||||||
|
const res = await request(app).get("/images/98439384");
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PUT /images/<id> works properly", () => {
|
||||||
|
let id: string;
|
||||||
|
let list: Image[];
|
||||||
|
beforeAll(async () => {
|
||||||
|
const res = await request(app).get("/images");
|
||||||
|
list = res.body.images;
|
||||||
|
id = list[0]._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 204 for valid modifications", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.put(`/images/${id}`)
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({ status: "available" });
|
||||||
|
expect(res.status).toBe(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 404 for non-existing ids", async () => {
|
||||||
|
const id = "000000000000000000000000"; // this was the least posible to exist ID
|
||||||
|
if (!(id in list)) {
|
||||||
|
const res = await request(app)
|
||||||
|
.put(`/images/${id}`)
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({ status: "available" });
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 400 for malformed ids", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.put("/images/98439384")
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({ status: "available" });
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.body).toEqual({ error: "Invalid Id" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 400 for malformed requests", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.put(`/images/${id}`)
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({ status: "wrong" });
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 409 if we try to modify the url into an existing one", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.put(`/images/${id}`)
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({ url: "https://test.url.com/2" });
|
||||||
|
expect(res.status).toBe(409);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 500 for an error on the service", async () => {
|
||||||
|
mock.module("../src/services/ImageService", () => ({
|
||||||
|
default: {
|
||||||
|
replaceOne: () => {
|
||||||
|
throw new Error("This is an expected testing error");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.put(`/images/${id}`)
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({ status: "available" });
|
||||||
|
|
||||||
|
expect(res.status).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 401 for unauthenticated requests", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.put(`/images/${id}`)
|
||||||
|
.send({ status: "available" });
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 403 for invalid tokens", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.put(`/images/${id}`)
|
||||||
|
.set("authorization", `Bearer token`)
|
||||||
|
.send({ status: "available" });
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have its changes be reflected onto the DB", async () => {
|
||||||
|
const image = await request(app)
|
||||||
|
.post("/images")
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({
|
||||||
|
url: "https://test.url.com/5",
|
||||||
|
status: "available",
|
||||||
|
tags: ["2girls", "touhou"],
|
||||||
|
});
|
||||||
|
const id = image.body.image._id;
|
||||||
|
await request(app)
|
||||||
|
.put(`/images/${id}`)
|
||||||
|
.set("authorization", `Bearer ${token}`)
|
||||||
|
.send({ status: "consumed" });
|
||||||
|
|
||||||
|
const res = await request(app).get(`/images/${id}`);
|
||||||
|
expect(res.body.image).toHaveProperty("status", "consumed");
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -0,0 +1,43 @@
|
||||||
|
import authModel from "../src/models/AuthModel";
|
||||||
|
import imageModel from "../src/models/ImageModel";
|
||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { UniqueEnforcer } from "enforce-unique";
|
||||||
|
|
||||||
|
const uniqueEnforcer = new UniqueEnforcer();
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
const images = faker.helpers.multiple(
|
||||||
|
() => ({
|
||||||
|
url: uniqueEnforcer.enforce(faker.internet.url),
|
||||||
|
status: faker.helpers.arrayElement([
|
||||||
|
"available",
|
||||||
|
"unavailable",
|
||||||
|
"consumed",
|
||||||
|
]),
|
||||||
|
tags: faker.helpers.arrayElements(
|
||||||
|
[
|
||||||
|
"2girls",
|
||||||
|
"sleeping",
|
||||||
|
"touhou",
|
||||||
|
"pokemon",
|
||||||
|
"closed_eyes",
|
||||||
|
"yume_nikki",
|
||||||
|
"waifu",
|
||||||
|
"computer",
|
||||||
|
"party",
|
||||||
|
"yuru_camp",
|
||||||
|
],
|
||||||
|
{ min: 0, max: 5 }
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
{ count: { min: 5, max: 50 } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait until all images are inserted
|
||||||
|
await Promise.allSettled(images.map((image) => imageModel.create(image)));
|
||||||
|
|
||||||
|
await authModel.create({
|
||||||
|
app: "tester",
|
||||||
|
secret: "test",
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue