+ gitignore

+ add boilerplate
This commit is contained in:
2026-07-18 15:03:13 +03:00
parent c2f561fe06
commit d562b07027
26 changed files with 891 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
.git
.gitignore
.gradle
.idea
.opencode
.swarm
build
*.md
docker-compose*.yml
.env*
+20
View File
@@ -0,0 +1,20 @@
# Database
DATABASE_JDBC_URL=jdbc:postgresql://localhost:5432/barotrauma-admin
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_POOL_SIZE=10
# Container Registry
REGISTRY_URL=your-registry.example.com
REGISTRY_USER=robot-user
REGISTRY_PASSWORD=your-token
# Deployment
DEPLOY_HOST=your-server.example.com
DEPLOY_PORT=22
DEPLOY_USER=deploy
DEPLOY_SSH_KEY=path/to/ssh-private-key
DEPLOY_PATH=/opt/barotrauma-admin
# Service
HOST_PORT=8080
+74
View File
@@ -0,0 +1,74 @@
name: Build & Deploy
run-name: ${{ gitea.event_name }} by ${{ gitea.actor }}
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master]
jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Grant Gradle executable
run: chmod +x gradlew
- name: Build
run: ./gradlew --no-daemon build
- name: Test
run: ./gradlew --no-daemon test
- name: Build distribution
run: ./gradlew --no-daemon installDist
- name: Login to Container Registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_URL }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build Docker image
run: |
docker build -t ${{ secrets.REGISTRY_URL }}/${{ gitea.repository }}:${{ gitea.sha }} -t ${{ secrets.REGISTRY_URL }}/${{ gitea.repository }}:latest .
- name: Push Docker image
run: |
docker push ${{ secrets.REGISTRY_URL }}/${{ gitea.repository }}:${{ gitea.sha }}
docker push ${{ secrets.REGISTRY_URL }}/${{ gitea.repository }}:latest
deploy:
runs-on: ubuntu-24.04
needs: build
if: gitea.ref == 'refs/heads/main' || gitea.ref == 'refs/heads/master'
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
port: ${{ secrets.DEPLOY_PORT || '22' }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
set -e
cd ${{ secrets.DEPLOY_PATH }}
cat > .env << EOF
REGISTRY_URL=${{ secrets.REGISTRY_URL }}
IMAGE_NAME=${{ gitea.repository }}
IMAGE_TAG=${{ gitea.sha }}
DATABASE_JDBC_URL=${{ secrets.DATABASE_JDBC_URL }}
DATABASE_USER=${{ secrets.DATABASE_USER }}
DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}
DATABASE_POOL_SIZE=${DATABASE_POOL_SIZE:-10}
HOST_PORT=${HOST_PORT:-8080}
EOF
docker compose -f docker-compose.remote.yml pull
docker compose -f docker-compose.remote.yml up -d --remove-orphans
docker image prune -f
+4
View File
@@ -0,0 +1,4 @@
/.idea/
/.opencode/
/.swarm/
/.kotlin/
@@ -0,0 +1,44 @@
# Codebase Review Swarm Skill v8.2
Portable Agent Skill for OpenCode, Codex, and Claude Code. It converts the v7 codebase-review swarm prompt into a progressive-disclosure skill package with a short routing-focused `SKILL.md`, detailed protocol references, parseable schemas, report template, optional Codex metadata, and deterministic helper scripts.
## Contents
```text
codebase-review-swarm/
SKILL.md
INSTALL.md
README.md
agents/
openai.yaml
assets/
jsonl-schemas.md
review-report-template.md
references/
compatibility-and-research-notes.md
full-v7-source-prompt.md
review-protocol-v8.2.md
scripts/
init-review-run.py
validate-skill-package.py
```
## Design summary
- Canonical opencode-swarm repo path: `.opencode/skills/codebase-review-swarm/`.
- Claude path: `.claude/skills/codebase-review-swarm/` as a thin adapter to the canonical OpenCode skill.
- Codex path: `.agents/skills/codebase-review-swarm/` as a thin adapter with `agents/openai.yaml`.
- Portable user install paths may still use `.agents/skills/`, `.opencode/skills/`, or `.claude/skills/` depending on host.
- Frontmatter is intentionally portable: required `name` and `description`, plus harmless metadata.
- Long instructions are split into references/assets to preserve routing quality and context budget.
- Focused track selections expand depth inside the selected domain; multi-track/all-track selections add waves rather than sacrificing per-track quality.
- The full v7 prompt is preserved verbatim for detailed track checklists.
- Standards are current as of 2026-06-08: ASVS 5.0.0, OWASP LLM Top 10 2025, SLSA v1.2, WCAG 2.2 AA, OpenTelemetry.
## Primary command
```text
$codebase-review-swarm
```
Begin at repository root. The skill runs Phase 0 inventory, stops for review mode selection unless preselected, then performs selected exhaustive tracks with coverage closure, review-depth planning, non-diluting multi-track execution, and critic validation.
+23
View File
@@ -0,0 +1,23 @@
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY gradlew settings.gradle.kts build.gradle.kts ./
COPY gradle gradle/
COPY gradle.properties ./
RUN ./gradlew --no-daemon dependencies 2>/dev/null || true
COPY src src/
RUN ./gradlew --no-daemon installDist
FROM eclipse-temurin:21-jre
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
bash ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/build/install/admin /app
EXPOSE 8080
ENTRYPOINT ["/app/bin/admin"]
+129
View File
@@ -0,0 +1,129 @@
# Barotrauma Admin
Ktor + Exposed + PostgreSQL backend for managing game-server tasks and executing remote commands.
---
## Project Structure
```
src/main/kotlin/com/nano/
├── main.kt # Application entry point + DI wiring
├── Routing.kt # HTTP routes (tasks, exec)
├── model/
│ ├── Task.kt # Task domain model + TaskStatus enum
│ └── ExecutorResult.kt
├── db/
│ ├── DatabaseConfig.kt # HikariCP + Exposed Database setup
│ └── Tables.kt # Exposed Table definitions (TaskTable)
├── repository/
│ └── TaskRepository.kt # CRUD operations on TaskTable, returns model objects
├── service/
│ └── TaskService.kt # Business logic: create, list, cancel tasks
└── executor/
└── Executor.kt # Bash command runner with log streaming
```
### Layer flow
```
Model → DB (connection + table) → Repository → Service → Route
```
---
## Local Development
```bash
# Start PostgreSQL + app
docker compose up --build
# Or run app without Docker (needs local Postgres)
./gradlew run
```
App is served at `http://localhost:8080`.
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `DATABASE_JDBC_URL` | `jdbc:postgresql://localhost:5432/barotrauma-admin` | PostgreSQL JDBC URL |
| `DATABASE_USER` | `postgres` | DB user |
| `DATABASE_PASSWORD` | `postgres` | DB password |
| `DATABASE_POOL_SIZE` | `10` | HikariCP pool size |
---
## API Endpoints
| Method | Path | Description |
|---|---|---|
| GET | `/` | Health check |
| GET | `/tasks` | List all tasks (`?status=RUNNING` to filter) |
| GET | `/tasks/{id}` | Get task by UUID |
| POST | `/tasks` | Create task `{"name":"...", "payload":"..."}` |
| POST | `/tasks/{id}/cancel` | Cancel task |
| DELETE | `/tasks/{id}` | Delete task |
| POST | `/exec` | Run bash command `{"command":"ls -la"}` |
---
## CI/CD (Gitea Actions)
The pipeline in `.gitea/workflows/ci.yaml` does:
1. **Build** — compiles the project, runs tests
2. **Docker** — builds image, pushes to registry
3. **Deploy** — SSHes to target host, pulls image, restarts via `docker-compose.remote.yml`
### Required secrets in Gitea
| Secret | Description |
|---|---|
| `REGISTRY_URL` | Container registry host (e.g. `gitea.example.com`) |
| `REGISTRY_USER` | Registry username |
| `REGISTRY_PASSWORD` | Registry token / password |
| `DEPLOY_HOST` | Remote server hostname |
| `DEPLOY_PORT` | SSH port (default 22) |
| `DEPLOY_USER` | Remote SSH user |
| `DEPLOY_SSH_KEY` | Private SSH key for deployment |
| `DEPLOY_PATH` | Target directory on remote host |
| `DATABASE_JDBC_URL` | Production JDBC URL |
| `DATABASE_USER` | Production DB user |
| `DATABASE_PASSWORD` | Production DB password |
### Required variables in Gitea
| Variable | Default | Description |
|---|---|---|
| `DATABASE_POOL_SIZE` | `10` | Connection pool size |
| `HOST_PORT` | `8080` | Host port mapping |
### Runner tag
Pipeline uses `runs-on: ubuntu-24.04`. Register your Gitea runner with:
```bash
gitea actions runner register --labels "ubuntu-24.04:docker://node:20-bookworm"
```
---
## Remote deployment
On the target host, place `docker-compose.remote.yml` and a `.env` file with the variables listed above.
```bash
docker compose -f docker-compose.remote.yml pull
docker compose -f docker-compose.remote.yml up -d
```
---
## Building locally
```bash
./gradlew build
./gradlew test
./gradlew installDist
```
+43
View File
@@ -0,0 +1,43 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
alias(ktorLibs.plugins.ktor)
}
group = "com.nano"
version = "1.0.0-SNAPSHOT"
application {
mainClass = "io.ktor.server.netty.EngineMain"
}
kotlin {
jvmToolchain(21)
}
dependencies {
implementation(ktorLibs.server.config.yaml)
implementation(ktorLibs.server.core)
implementation(ktorLibs.server.netty)
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.server.call.logging)
implementation(libs.ktor.server.call.id)
implementation(libs.logback.classic)
implementation(libs.exposed.core)
implementation(libs.exposed.dao)
implementation(libs.exposed.jdbc)
implementation(libs.hikari)
implementation(libs.postgresql)
testImplementation(kotlin("test"))
testImplementation(ktorLibs.server.testHost)
testImplementation("io.ktor:ktor-server-content-negotiation:3.5.0")
testImplementation("io.ktor:ktor-serialization-kotlinx-json:3.5.0")
testImplementation("com.h2database:h2:2.3.232")
testImplementation("org.jetbrains.exposed:exposed-core:0.57.0")
testImplementation("org.jetbrains.exposed:exposed-jdbc:0.57.0")
}
+16
View File
@@ -0,0 +1,16 @@
services:
admin:
image: ${REGISTRY_URL}/${IMAGE_NAME}:${IMAGE_TAG:-latest}
restart: unless-stopped
ports:
- "${HOST_PORT:-8080}:8080"
environment:
DATABASE_JDBC_URL: ${DATABASE_JDBC_URL}
DATABASE_USER: ${DATABASE_USER}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
DATABASE_POOL_SIZE: "${DATABASE_POOL_SIZE:-10}"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
+34
View File
@@ -0,0 +1,34 @@
services:
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: barotrauma-admin
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d barotrauma-admin"]
interval: 5s
timeout: 5s
retries: 5
admin:
build: .
restart: unless-stopped
ports:
- "8080:8080"
environment:
DATABASE_JDBC_URL: jdbc:postgresql://db:5432/barotrauma-admin
DATABASE_USER: postgres
DATABASE_PASSWORD: postgres
DATABASE_POOL_SIZE: "10"
depends_on:
db:
condition: service_healthy
volumes:
pgdata:
+2
View File
@@ -0,0 +1,2 @@
kotlin.code.style=official
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+22
View File
@@ -0,0 +1,22 @@
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositories {
mavenCentral()
}
versionCatalogs {
create("ktorLibs").from("io.ktor:ktor-version-catalog:3.5.0")
}
}
rootProject.name = "admin"
+98
View File
@@ -0,0 +1,98 @@
package com.nano
import com.nano.executor.Executor
import com.nano.model.TaskStatus
import com.nano.service.TaskService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import java.util.UUID
private object UUIDSerializer : kotlinx.serialization.KSerializer<UUID> {
override val descriptor = kotlinx.serialization.descriptors.PrimitiveSerialDescriptor("UUID", kotlinx.serialization.descriptors.PrimitiveKind.STRING)
override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: UUID) = encoder.encodeString(value.toString())
override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): UUID = UUID.fromString(decoder.decodeString())
}
@Serializable
data class CreateTaskRequest(val name: String, val payload: String = "")
@Serializable
data class ExecCommandRequest(val command: String)
fun Application.configureRouting(taskService: TaskService, executor: Executor) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
serializersModule = SerializersModule {
contextual(UUIDSerializer)
}
})
}
routing {
get("/") {
call.respondText("Barotrauma Admin API")
}
route("/tasks") {
get {
val statusParam = call.request.queryParameters["status"]
val status = statusParam?.let { TaskStatus.valueOf(it.uppercase()) }
call.respond(taskService.listTasks(status))
}
get("/{id}") {
val id = UUID.fromString(call.parameters["id"])
val task = taskService.getTask(id)
if (task != null) call.respond(task)
else call.respondText("Not Found", status = HttpStatusCode.NotFound)
}
post {
val body = call.receive<CreateTaskRequest>()
val task = taskService.createTask(body.name, body.payload)
call.respond(HttpStatusCode.Created, task)
}
post("/{id}/cancel") {
val id = UUID.fromString(call.parameters["id"])
val task = taskService.updateStatus(id, TaskStatus.CANCELLED)
if (task != null) call.respond(task)
else call.respondText("Not Found", status = HttpStatusCode.NotFound)
}
delete("/{id}") {
val id = UUID.fromString(call.parameters["id"])
val deleted = taskService.deleteTask(id)
if (deleted) call.respondText("Deleted")
else call.respondText("Not Found", status = HttpStatusCode.NotFound)
}
}
route("/exec") {
post {
val body = call.receive<ExecCommandRequest>()
val logs = mutableListOf<String>()
val result = executor.execute(body.command, onLog = { logs.add(it) })
call.respond(
mapOf(
"exitCode" to result.exitCode,
"success" to result.isSuccess,
"stdout" to result.stdout,
"stderr" to result.stderr,
"logs" to logs
)
)
}
}
}
}
@@ -0,0 +1,38 @@
package com.nano.db
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.Database
import javax.sql.DataSource
object DatabaseConfig {
private var dataSource: DataSource? = null
fun connect(
jdbcUrl: String = System.getenv("DATABASE_JDBC_URL")
?: "jdbc:postgresql://localhost:5432/barotrauma-admin",
driver: String = "org.postgresql.Driver",
user: String = System.getenv("DATABASE_USER") ?: "postgres",
password: String = System.getenv("DATABASE_PASSWORD") ?: "postgres",
poolSize: Int = (System.getenv("DATABASE_POOL_SIZE") ?: "10").toInt()
): Database {
val config = HikariConfig().apply {
this.jdbcUrl = jdbcUrl
this.driverClassName = driver
this.username = user
this.password = password
maximumPoolSize = poolSize
isAutoCommit = false
transactionIsolation = "TRANSACTION_REPEATABLE_READ"
validate()
}
val ds = HikariDataSource(config)
dataSource = ds
return Database.connect(ds)
}
fun shutdown() {
(dataSource as? HikariDataSource)?.close()
}
}
+16
View File
@@ -0,0 +1,16 @@
package com.nano.db
import org.jetbrains.exposed.sql.Table
import java.util.UUID
object TaskTable : Table("tasks") {
val id = uuid("id").autoGenerate()
val name = varchar("name", 255)
val payload = text("payload").default("")
val status = enumerationByName("status", 32, com.nano.model.TaskStatus::class)
.default(com.nano.model.TaskStatus.PENDING)
val createdAt = long("created_at")
val updatedAt = long("updated_at")
override val primaryKey = PrimaryKey(id)
}
@@ -0,0 +1,61 @@
package com.nano.executor
import com.nano.model.ExecutorResult
import java.io.BufferedReader
import java.io.InputStreamReader
class Executor {
fun execute(
command: String,
onLog: ((String) -> Unit)? = null,
workDir: String? = null,
env: Map<String, String> = emptyMap(),
): ExecutorResult {
val builder =
ProcessBuilder()
.command("bash", "-c", command)
if (workDir != null) {
builder.directory(java.io.File(workDir))
}
builder.environment().putAll(env)
builder.redirectErrorStream(false)
val process = builder.start()
val stdoutBuilder = StringBuilder()
val stderrBuilder = StringBuilder()
val stdoutThread =
Thread {
BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
reader.lines().forEach { line ->
stdoutBuilder.appendLine(line)
onLog?.invoke(line)
}
}
}
val stderrThread =
Thread {
BufferedReader(InputStreamReader(process.errorStream)).use { reader ->
reader.lines().forEach { line ->
stderrBuilder.appendLine(line)
onLog?.invoke("[ERR] $line")
}
}
}
stdoutThread.start()
stderrThread.start()
val exitCode = process.waitFor()
stdoutThread.join()
stderrThread.join()
return ExecutorResult(
exitCode = exitCode,
stdout = stdoutBuilder.toString().trimEnd(),
stderr = stderrBuilder.toString().trimEnd(),
)
}
}
+29
View File
@@ -0,0 +1,29 @@
package com.nano
import com.nano.db.DatabaseConfig
import com.nano.db.TaskTable
import com.nano.executor.Executor
import com.nano.repository.TaskRepository
import com.nano.service.TaskService
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
fun main(args: Array<String>) {
EngineMain.main(args)
}
fun Application.module() {
val db = DatabaseConfig.connect()
transaction(db) {
SchemaUtils.createMissingTablesAndColumns(TaskTable)
}
val taskRepository = TaskRepository(db)
val taskService = TaskService(taskRepository)
val executor = Executor()
configureRouting(taskService, executor)
}
@@ -0,0 +1,11 @@
package com.nano.model
import kotlinx.serialization.Serializable
@Serializable
data class ExecutorResult(
val exitCode: Int,
val stdout: String,
val stderr: String,
val isSuccess: Boolean = exitCode == 0,
)
+28
View File
@@ -0,0 +1,28 @@
package com.nano.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.time.Instant
import java.util.UUID
@Serializable
data class Task(
@Contextual val id: UUID = UUID.randomUUID(),
val name: String,
val payload: String = "",
val status: TaskStatus = TaskStatus.PENDING,
val createdAt: Long = Instant.now().toEpochMilli(),
val updatedAt: Long = Instant.now().toEpochMilli()
) {
val createdAtInstant: Instant get() = Instant.ofEpochMilli(createdAt)
val updatedAtInstant: Instant get() = Instant.ofEpochMilli(updatedAt)
}
@Serializable
enum class TaskStatus {
PENDING,
RUNNING,
COMPLETED,
FAILED,
CANCELLED
}
@@ -0,0 +1,87 @@
package com.nano.repository
import com.nano.db.TaskTable
import com.nano.model.Task
import com.nano.model.TaskStatus
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.Instant
import java.util.UUID
class TaskRepository(
private val db: Database,
) {
fun create(task: Task): Task =
transaction(db) {
val now = Instant.now().toEpochMilli()
TaskTable.insert {
it[id] = task.id
it[name] = task.name
it[payload] = task.payload
it[status] = task.status
it[createdAt] = now
it[updatedAt] = now
}
task.copy(createdAt = now, updatedAt = now)
}
fun findById(id: UUID): Task? =
transaction(db) {
TaskTable
.selectAll()
.where(TaskTable.id eq id)
.singleOrNull()
?.toTask()
}
fun findAll(): List<Task> =
transaction(db) {
TaskTable
.selectAll()
.orderBy(TaskTable.createdAt, SortOrder.DESC)
.map { it.toTask() }
}
fun findByStatus(status: TaskStatus): List<Task> =
transaction(db) {
TaskTable
.selectAll()
.where(TaskTable.status eq status)
.orderBy(TaskTable.createdAt, SortOrder.DESC)
.map { it.toTask() }
}
fun updateStatus(
id: UUID,
newStatus: TaskStatus,
): Task? =
transaction(db) {
val now = Instant.now().toEpochMilli()
val updated =
TaskTable.update({ TaskTable.id eq id }) {
it[status] = newStatus
it[updatedAt] = now
}
if (updated == 0) {
null
} else {
findById(id)
}
}
fun delete(id: UUID): Boolean =
transaction(db) {
TaskTable.deleteWhere { TaskTable.id eq id } > 0
}
private fun ResultRow.toTask() =
Task(
id = this[TaskTable.id],
name = this[TaskTable.name],
payload = this[TaskTable.payload],
status = this[TaskTable.status],
createdAt = this[TaskTable.createdAt],
updatedAt = this[TaskTable.updatedAt],
)
}
@@ -0,0 +1,25 @@
package com.nano.service
import com.nano.model.Task
import com.nano.model.TaskStatus
import com.nano.repository.TaskRepository
import java.util.UUID
class TaskService(private val repository: TaskRepository) {
fun createTask(name: String, payload: String = ""): Task {
val task = Task(name = name, payload = payload)
return repository.create(task)
}
fun getTask(id: UUID): Task? = repository.findById(id)
fun listTasks(status: TaskStatus? = null): List<Task> =
if (status != null) repository.findByStatus(status)
else repository.findAll()
fun updateStatus(id: UUID, status: TaskStatus): Task? =
repository.updateStatus(id, status)
fun deleteTask(id: UUID): Boolean = repository.delete(id)
}
+6
View File
@@ -0,0 +1,6 @@
ktor:
deployment:
port: 8080
application:
modules:
- com.nano.MainKt.module
+12
View File
@@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>
+50
View File
@@ -0,0 +1,50 @@
package com.nano
import com.nano.db.TaskTable
import com.nano.executor.Executor
import com.nano.repository.TaskRepository
import com.nano.service.TaskService
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.server.testing.testApplication
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import kotlin.test.*
class ServerTest {
@Test
fun `root endpoint returns OK`() = testApplication {
val db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "org.h2.Driver")
transaction(db) { SchemaUtils.createMissingTablesAndColumns(TaskTable) }
val taskService = TaskService(TaskRepository(db))
application {
configureRouting(taskService, Executor())
}
assertEquals(HttpStatusCode.OK, client.get("/").status)
}
@Test
fun `create task returns Created`() = testApplication {
val db = Database.connect("jdbc:h2:mem:test2;DB_CLOSE_DELAY=-1", "org.h2.Driver")
transaction(db) { SchemaUtils.createMissingTablesAndColumns(TaskTable) }
val taskService = TaskService(TaskRepository(db))
application {
configureRouting(taskService, Executor())
}
val response = client.post("/tasks") {
contentType(ContentType.Application.Json)
setBody("""{"name":"test-task","payload":"hello"}""")
}
assertEquals(HttpStatusCode.Created, response.status)
}
}