mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 11:30:12 +08:00
Merge pull request #1 from lancatlin/server-justin
Add Quests and Sessions API
This commit is contained in:
29 files changed
+2638
-45
No files matched your search
@@ -1,2 +1,3 @@
|
|||||||
vite.config.js
|
vite.config.js
|
||||||
.gitignore
|
.gitignore
|
||||||
|
data
|
||||||
@@ -2,6 +2,7 @@ module.exports = {
|
|||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
|
jest: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'plugin:vue/vue3-essential',
|
'plugin:vue/vue3-essential',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pids
|
|||||||
*.pid
|
*.pid
|
||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
|
data
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib-cov
|
lib-cov
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
data/*
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
# Linux Odyssey
|
# Linux Odyssey
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- NodeJS
|
||||||
|
- Docker
|
||||||
|
- Yarn
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -8,6 +14,12 @@ cd linux-odyssey
|
|||||||
yarn install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Create database
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
## Server
|
## Server
|
||||||
|
|
||||||
Source: `server/`
|
Source: `server/`
|
||||||
@@ -25,3 +37,11 @@ Source: `app/`
|
|||||||
yarn app
|
yarn app
|
||||||
# Open http://localhost:5173/
|
# Open http://localhost:5173/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## CLI terminal client
|
||||||
|
|
||||||
|
Open server first, then
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn cli
|
||||||
|
```
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mongo
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- '27017:27017'
|
||||||
|
volumes:
|
||||||
|
- ./data:/data/db
|
||||||
+2
-1
@@ -13,8 +13,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"app": "yarn workspace app dev",
|
"app": "yarn workspace app dev",
|
||||||
"server": "yarn workspace server dev",
|
"server": "yarn workspace server dev",
|
||||||
|
"cli": "yarn workspace server cli",
|
||||||
"lint": "eslint --ext .js,.vue .",
|
"lint": "eslint --ext .js,.vue .",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --ignore-path .prettierignore --write app server"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/eslint-config-prettier": "^7.1.0",
|
"@vue/eslint-config-prettier": "^7.1.0",
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import http from 'http'
|
|
||||||
import socketServer from './socket.js'
|
|
||||||
|
|
||||||
const port = 3000
|
|
||||||
const app = express()
|
|
||||||
const server = http.createServer(app)
|
|
||||||
socketServer(server)
|
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.send('Hello World!')
|
|
||||||
})
|
|
||||||
|
|
||||||
server.listen(port, () => {
|
|
||||||
console.log(`Server listening at http://localhost:${port}`)
|
|
||||||
})
|
|
||||||
+10
-3
@@ -4,16 +4,23 @@
|
|||||||
"description": "Linux Odyssey Server",
|
"description": "Linux Odyssey Server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node src/index.js",
|
||||||
"dev": "nodemon index.js"
|
"dev": "nodemon src/index.js",
|
||||||
|
"cli": "node src/client.js",
|
||||||
|
"test": "yarn node --experimental-vm-modules $(yarn bin jest)"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dockerode": "^3.3.5",
|
"dockerode": "^3.3.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"marked": "^5.1.1",
|
||||||
|
"mongoose": "^7.4.0",
|
||||||
"socket.io": "^4.7.1",
|
"socket.io": "^4.7.1",
|
||||||
"socket.io-client": "^4.7.1"
|
"socket.io-client": "^4.7.1",
|
||||||
|
"swagger-ui-express": "^5.0.0",
|
||||||
|
"yaml": "^2.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"jest": "^29.6.1",
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: Hello World
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hello World
|
||||||
|
|
||||||
|
Welcome to your first quest! This quest will help you familiarize yourself with the terminal.
|
||||||
|
|
||||||
|
The terminal is a text-based interface that allows you to interact directly with your computer's operating system. You can perform various tasks by typing commands into the terminal and pressing `Enter` to execute them.
|
||||||
|
|
||||||
|
For your first task, you are going to use the `echo` command.
|
||||||
|
|
||||||
|
## Task 1: Using the `echo` Command
|
||||||
|
|
||||||
|
The `echo` command in Unix is used to display line of text/string that are passed as an argument. This is a built in command that is mostly used in shell scripts and batch files to output status text to the screen or a file.
|
||||||
|
|
||||||
|
Here is what you need to do:
|
||||||
|
|
||||||
|
1. Open your terminal.
|
||||||
|
2. Type `echo Hello World` and hit `Enter`.
|
||||||
|
3. You should see `Hello World` output to the terminal. This is because `echo` prints its arguments to the standard output, which is your terminal by default.
|
||||||
|
|
||||||
|
Congratulation! You've just run your first terminal command!
|
||||||
|
|
||||||
|
Stay tuned for the next quests where you'll delve deeper into more complex and powerful terminal commands.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Quest from '../../models/quest.js'
|
||||||
|
|
||||||
|
export async function getQuests(req, res) {
|
||||||
|
const quests = await Quest.find({}).sort({ order: 1 })
|
||||||
|
res.json(
|
||||||
|
quests.map(({ name, title, order, _id }) => ({
|
||||||
|
_id,
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
order,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQuestDetail(req, res) {
|
||||||
|
const { id } = req.params
|
||||||
|
const quest = await Quest.findById(id)
|
||||||
|
if (!quest) {
|
||||||
|
res.status(404).json({ message: 'Quest not found.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.json(quest)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import Session from '../../models/session.js'
|
||||||
|
import Quest from '../../models/quest.js'
|
||||||
|
|
||||||
|
export async function getSessionList(req, res) {
|
||||||
|
try {
|
||||||
|
const query = { user: req.user.id }
|
||||||
|
if (req.query.quest_id) {
|
||||||
|
query.quest = req.query.quest_id
|
||||||
|
}
|
||||||
|
const sessions = await Session.find(query)
|
||||||
|
res.json(sessions)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
res.status(500).json({ message: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSession(req, res) {
|
||||||
|
const quest = await Quest.findById(req.body.quest_id)
|
||||||
|
if (!quest) {
|
||||||
|
res.status(400).json({ message: 'Quest not found.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSession = new Session({
|
||||||
|
user: req.user,
|
||||||
|
quest,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await newSession.save()
|
||||||
|
res.status(201).json(session)
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ message: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessionById(req, res) {
|
||||||
|
try {
|
||||||
|
const session = await Session.findOne({
|
||||||
|
_id: req.params.id,
|
||||||
|
user: req.user.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
res.status(404).json({ message: 'Session not found.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(session)
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSessionById(req, res) {
|
||||||
|
try {
|
||||||
|
const session = await Session.findOneAndDelete({
|
||||||
|
_id: req.params.id,
|
||||||
|
user: req.user.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
res.status(404).json({ message: 'Session not found.' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).end()
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
import sessions from './sessions.js'
|
||||||
|
import quests from './quests.js'
|
||||||
|
import { authMiddleware } from '../../middleware/auth.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.send('Hello API!')
|
||||||
|
})
|
||||||
|
|
||||||
|
router.use('/sessions', authMiddleware, sessions)
|
||||||
|
router.use('/quests', quests)
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
import * as questController from '../controllers/questController.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router.get('/', questController.getQuests)
|
||||||
|
router.get('/:id', questController.getQuestDetail)
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
import * as sessionController from '../controllers/sessionController.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router.get('/', sessionController.getSessionList)
|
||||||
|
router.post('/', sessionController.createSession)
|
||||||
|
router.get('/:id', sessionController.getSessionById)
|
||||||
|
router.delete('/:id', sessionController.deleteSessionById)
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Server } from 'socket.io'
|
import { Server } from 'socket.io'
|
||||||
import { getOrCreateContainer, attachContainer } from './docker.js'
|
import { getOrCreateContainer, attachContainer } from '../utils/docker.js'
|
||||||
|
|
||||||
export default (server) => {
|
export default (server) => {
|
||||||
const io = new Server(server)
|
const io = new Server(server)
|
||||||
File renamed without changes.
@@ -0,0 +1,10 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
export default async function connectDB() {
|
||||||
|
try {
|
||||||
|
await mongoose.connect('mongodb://localhost:27017/odyssey-test')
|
||||||
|
console.log('Connected to MongoDB')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import fs from 'fs/promises'
|
||||||
|
import express from 'express'
|
||||||
|
import http from 'http'
|
||||||
|
import YAML from 'yaml'
|
||||||
|
import swaggerUI from 'swagger-ui-express'
|
||||||
|
|
||||||
|
import socketServer from './api/socket.js'
|
||||||
|
import connectDB from './db.js'
|
||||||
|
import apiRouter from './api/routes/index.js'
|
||||||
|
import loadAndUpdateQuests from './utils/quest.js'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const port = 3000
|
||||||
|
const app = express()
|
||||||
|
const server = http.createServer(app)
|
||||||
|
socketServer(server)
|
||||||
|
await connectDB()
|
||||||
|
await loadAndUpdateQuests()
|
||||||
|
|
||||||
|
const file = await fs.readFile('./swagger.yaml', 'utf8')
|
||||||
|
const swaggerDocument = YAML.parse(file)
|
||||||
|
app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(swaggerDocument))
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('Hello World!')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/api/v1', apiRouter)
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(`Server listening at http://localhost:${port}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => console.log(err))
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import User from '../models/user.js'
|
||||||
|
|
||||||
|
export async function authMiddleware(req, res, next) {
|
||||||
|
try {
|
||||||
|
let user = await User.findOne({ username: 'defaultUser' })
|
||||||
|
if (!user) {
|
||||||
|
user = new User({
|
||||||
|
username: 'defaultUser',
|
||||||
|
email: 'john@example.com',
|
||||||
|
})
|
||||||
|
await user.save()
|
||||||
|
}
|
||||||
|
req.user = user
|
||||||
|
next()
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -0,0 +1,13 @@
|
|||||||
|
import { model, Schema } from 'mongoose'
|
||||||
|
|
||||||
|
const Quest = model(
|
||||||
|
'Quest',
|
||||||
|
new Schema({
|
||||||
|
_id: String,
|
||||||
|
title: String,
|
||||||
|
order: Number,
|
||||||
|
content: String,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Quest
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { model, Schema } from 'mongoose'
|
||||||
|
|
||||||
|
const Session = model(
|
||||||
|
'Session',
|
||||||
|
new Schema(
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
type: Schema.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
quest: {
|
||||||
|
type: String,
|
||||||
|
ref: 'Quest',
|
||||||
|
},
|
||||||
|
containerId: String,
|
||||||
|
finishedAt: Date,
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Session
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { model } from 'mongoose'
|
||||||
|
|
||||||
|
const User = model('User', {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
email: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default User
|
||||||
File renamed without changes.
@@ -0,0 +1,54 @@
|
|||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
import { Marked } from 'marked'
|
||||||
|
import yaml from 'yaml'
|
||||||
|
|
||||||
|
import Quest from '../models/quest.js'
|
||||||
|
|
||||||
|
const marked = new Marked({
|
||||||
|
mangle: false,
|
||||||
|
headerIds: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const questDirectory = path.join(process.cwd(), 'quests')
|
||||||
|
|
||||||
|
export default async function loadAndUpdateQuests() {
|
||||||
|
const questNames = await fs.readdir(questDirectory)
|
||||||
|
|
||||||
|
const quests = Promise.all(
|
||||||
|
questNames.map(async (id) => {
|
||||||
|
const fullPath = path.join(questDirectory, id, 'README.md')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read and parse the README.md file.
|
||||||
|
const markdown = await fs.readFile(fullPath, 'utf-8')
|
||||||
|
|
||||||
|
// Extract and parse YAML frontmatter.
|
||||||
|
const match = /---\n([\s\S]+?)\n---/.exec(markdown)
|
||||||
|
const metadata = yaml.parse(match[1])
|
||||||
|
const content = markdown.slice(match[0].length)
|
||||||
|
const htmlContent = marked.parse(content)
|
||||||
|
|
||||||
|
const { title, order } = metadata
|
||||||
|
|
||||||
|
return Quest.findByIdAndUpdate(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
_id: id,
|
||||||
|
title,
|
||||||
|
order,
|
||||||
|
content: htmlContent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upsert: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error parsing quest ${id}:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return quests
|
||||||
|
}
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
openapi: 3.0.0
|
||||||
|
servers:
|
||||||
|
- url: /api/v1
|
||||||
|
info:
|
||||||
|
description: Linux Odyssey API
|
||||||
|
version: '1.0.0'
|
||||||
|
title: Session API
|
||||||
|
contact:
|
||||||
|
email: wancat@wancat.cc
|
||||||
|
license:
|
||||||
|
name: Apache 2.0
|
||||||
|
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
||||||
|
tags:
|
||||||
|
- name: admins
|
||||||
|
description: Secured Admin-only calls
|
||||||
|
- name: developers
|
||||||
|
description: Operations available to regular developers
|
||||||
|
paths:
|
||||||
|
/sessions:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- signed-in
|
||||||
|
summary: list existing sessions of current user
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: quest_id
|
||||||
|
description: find sessions by quest id
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: list of existing session
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/SessionSummary'
|
||||||
|
|
||||||
|
post:
|
||||||
|
summary: create a new session for current user
|
||||||
|
tags:
|
||||||
|
- signed-in
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
description: session data
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
quest_id:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: session created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Session'
|
||||||
|
'400':
|
||||||
|
description: Invalid input
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/sessions/{session_id}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- signed-in
|
||||||
|
summary: get a specific session of current user by ID
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: session_id
|
||||||
|
description: ID of the session to retrieve
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: objectId
|
||||||
|
example: 64b7a3a3c182b4826dd1aaa1
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: the session
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Session'
|
||||||
|
'404':
|
||||||
|
description: cannot find specific session
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- signed-in
|
||||||
|
summary: delete a specific session by ID
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: session_id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: objectId
|
||||||
|
example: 64b7a3a3c182b4826dd1aaa1
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Session successfully deleted
|
||||||
|
|
||||||
|
'404':
|
||||||
|
description: Session not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: 'Session not found.'
|
||||||
|
'401':
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: 'Unauthorized.'
|
||||||
|
|
||||||
|
/register:
|
||||||
|
post:
|
||||||
|
summary: Register a new user
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: User created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: User created successfully
|
||||||
|
'400':
|
||||||
|
description: Bad Request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: Missing username, email or password
|
||||||
|
'409':
|
||||||
|
description: Conflict
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: User already exists
|
||||||
|
|
||||||
|
/login:
|
||||||
|
post:
|
||||||
|
summary: Log in a user
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User logged in
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: Login successful
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: Access token
|
||||||
|
'400':
|
||||||
|
description: Bad Request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: Missing username or password
|
||||||
|
'401':
|
||||||
|
description: Unauthorized
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: Incorrect username or password
|
||||||
|
|
||||||
|
/profile:
|
||||||
|
get:
|
||||||
|
summary: Get the profile of current logged-in user
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Get user's profile
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
|
||||||
|
/quests:
|
||||||
|
get:
|
||||||
|
summary: Get quests list
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Quests summary list
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/QuestSummary'
|
||||||
|
|
||||||
|
/quests/{quest_id}:
|
||||||
|
get:
|
||||||
|
summary: get a quest's detail by name
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: quest_id
|
||||||
|
description: name of the quest to retrieve
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: the quest
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Quest'
|
||||||
|
'404':
|
||||||
|
description: cannot find specific quest
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
_id:
|
||||||
|
type: string
|
||||||
|
format: objectId
|
||||||
|
example: 64b7a3a3c182b4826dd1aaa1
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
example: 'alex'
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
Quest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
_id:
|
||||||
|
type: string
|
||||||
|
example: helloworld
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
example: Hello World
|
||||||
|
content:
|
||||||
|
type: string
|
||||||
|
format: html
|
||||||
|
example: '<h1>Hello World</h1>'
|
||||||
|
order:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
QuestSummary:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
_id:
|
||||||
|
type: string
|
||||||
|
example: helloworld
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
example: Hello World
|
||||||
|
order:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
Session:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
_id:
|
||||||
|
type: string
|
||||||
|
format: objectId
|
||||||
|
example: 64b7a3a3c182b4826dd1aaa1
|
||||||
|
user:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
quest:
|
||||||
|
$ref: '#/components/schemas/Quest'
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
finished_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
terminals:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
graphs:
|
||||||
|
$ref: '#/components/schemas/TreeNode'
|
||||||
|
progress:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Progress'
|
||||||
|
hints:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Hint'
|
||||||
|
|
||||||
|
SessionSummary:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
_id:
|
||||||
|
type: string
|
||||||
|
format: objectId
|
||||||
|
example: 64b7a3a3c182b4826dd1aaa1
|
||||||
|
quest:
|
||||||
|
$ref: '#/components/schemas/Quest'
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
finished_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
TreeNode:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
display:
|
||||||
|
type: boolean
|
||||||
|
children:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/TreeNode'
|
||||||
|
|
||||||
|
Progress:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
finished_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
skipped_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
Hint:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
content:
|
||||||
|
type: string
|
||||||
|
source:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
securitySchemes:
|
||||||
|
BearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import jest from 'jest'
|
||||||
|
import * as sessionController from '../../src/api/controllers/sessionController'
|
||||||
|
import Session from '../../src/models/session'
|
||||||
|
|
||||||
|
jest.mock('../../src/models/session')
|
||||||
|
|
||||||
|
describe('Session Controller', () => {
|
||||||
|
let mockReq
|
||||||
|
let mockRes
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReq = {
|
||||||
|
user: { id: 'userid' },
|
||||||
|
body: {},
|
||||||
|
params: {},
|
||||||
|
}
|
||||||
|
mockRes = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
json: jest.fn().mockReturnThis(),
|
||||||
|
end: jest.fn().mockReturnThis(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getSessionList returns a list of sessions', async () => {
|
||||||
|
Session.find.mockResolvedValue(['session1', 'session2'])
|
||||||
|
await sessionController.getSessionList(mockReq, mockRes)
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith(['session1', 'session2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createSession creates a new session', async () => {
|
||||||
|
mockReq.body.exercise_id = 'exerciseid'
|
||||||
|
const mockSession = { user: 'userid', exercise: 'exerciseid' }
|
||||||
|
Session.mockReturnValue(mockSession)
|
||||||
|
mockSession.save = jest.fn().mockResolvedValue('newsess')
|
||||||
|
await sessionController.createSession(mockReq, mockRes)
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(201)
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith('newsess')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getSessionById returns the correct session', async () => {
|
||||||
|
mockReq.params.session_id = 'sessionid'
|
||||||
|
const mockSession = { user: 'userid', _id: 'sessionid' }
|
||||||
|
Session.findOne.mockResolvedValue(mockSession)
|
||||||
|
await sessionController.getSessionById(mockReq, mockRes)
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith(mockSession)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('deleteSessionById deletes a session', async () => {
|
||||||
|
mockReq.params.session_id = 'sessionid'
|
||||||
|
const mockSession = { user: 'userid', _id: 'sessionid' }
|
||||||
|
Session.findOneAndDelete.mockResolvedValue(mockSession)
|
||||||
|
await sessionController.deleteSessionById(mockReq, mockRes)
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(204)
|
||||||
|
expect(mockRes.end).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in new issue
Block a user