WIP: tests, REST API support (auth).
This commit is contained in:
parent
2551affd4b
commit
b5a70f6de0
2
api.rst
2
api.rst
@ -1,5 +1,5 @@
|
|||||||
✓ GET /api/ping
|
✓ GET /api/ping
|
||||||
- POST /api/auth-token
|
- GET /api/auth-token
|
||||||
✓ GET /api/projects -- return project summaries
|
✓ GET /api/projects -- return project summaries
|
||||||
- POST /api/projects -- create a new project
|
- POST /api/projects -- create a new project
|
||||||
- GET /api/project/<proj-id> -- return detailed project record (include steps)
|
- GET /api/project/<proj-id> -- return detailed project record (include steps)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import docopt, os, sequtils, tempfile
|
import docopt, os, sequtils, tempfile
|
||||||
|
|
||||||
import strawboss/private/util
|
import strawbosspkg/private/util
|
||||||
import strawboss/configuration
|
import strawbosspkg/configuration
|
||||||
import strawboss/core
|
import strawbosspkg/core
|
||||||
import strawboss/server
|
import strawbosspkg/server
|
||||||
|
|
||||||
let SB_VER = "0.2.0"
|
let SB_VER = "0.2.0"
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ type
|
|||||||
artifacts*, cmdInput*, depends*, expectedEnv*: seq[string]
|
artifacts*, cmdInput*, depends*, expectedEnv*: seq[string]
|
||||||
dontSkip*: bool
|
dontSkip*: bool
|
||||||
|
|
||||||
ProjectCfg* = object
|
ProjectConfig* = object
|
||||||
name*: string
|
name*: string
|
||||||
versionCmd*: string
|
versionCmd*: string
|
||||||
steps*: Table[string, Step]
|
steps*: Table[string, Step]
|
||||||
@ -37,15 +37,31 @@ type
|
|||||||
projects*: seq[ProjectDef]
|
projects*: seq[ProjectDef]
|
||||||
users*: seq[UserRef]
|
users*: seq[UserRef]
|
||||||
|
|
||||||
|
# Equality on custom types
|
||||||
|
proc `==`*(a, b: UserRef): bool = result = a.name == b.name
|
||||||
|
|
||||||
|
proc `==`*(a, b: ProjectDef): bool =
|
||||||
|
if a.envVars.len != b.envVars.len: return false
|
||||||
|
|
||||||
|
for k, v in a.envVars:
|
||||||
|
if not b.envVars.hasKey(k) or a.envVars[k] != b.envVars[k]: return false
|
||||||
|
|
||||||
|
return
|
||||||
|
a.name == b.name and
|
||||||
|
a.cfgFilePath == b.cfgFilePath and
|
||||||
|
a.defaultBranch == b.defaultBranch and
|
||||||
|
a.repo == b.repo
|
||||||
|
|
||||||
# internal utils
|
# internal utils
|
||||||
|
|
||||||
let nullNode = newJNull()
|
let nullNode = newJNull()
|
||||||
proc getIfExists(n: JsonNode, key: string): JsonNode =
|
proc getIfExists(n: JsonNode, key: string): JsonNode =
|
||||||
|
# convenience method to get a key from a JObject or return null
|
||||||
result = if n.hasKey(key): n[key]
|
result = if n.hasKey(key): n[key]
|
||||||
else: nullNode
|
else: nullNode
|
||||||
|
|
||||||
proc getOrFail(n: JsonNode, key: string, objName: string = ""): JsonNode =
|
proc getOrFail(n: JsonNode, key: string, objName: string = ""): JsonNode =
|
||||||
|
# convenience method to get a key from a JObject or raise an exception
|
||||||
if not n.hasKey(key): raiseEx objName & " missing key " & key
|
if not n.hasKey(key): raiseEx objName & " missing key " & key
|
||||||
return n[key]
|
return n[key]
|
||||||
|
|
||||||
@ -84,7 +100,7 @@ proc loadStrawBossConfig*(cfgFile: string): StrawBossConfig =
|
|||||||
projects: projectDefs,
|
projects: projectDefs,
|
||||||
users: users)
|
users: users)
|
||||||
|
|
||||||
proc loadProjectConfig*(cfgFile: string): ProjectCfg =
|
proc loadProjectConfig*(cfgFile: string): ProjectConfig =
|
||||||
if not existsFile(cfgFile):
|
if not existsFile(cfgFile):
|
||||||
raiseEx "project config file not found: " & cfgFile
|
raiseEx "project config file not found: " & cfgFile
|
||||||
|
|
||||||
@ -103,12 +119,12 @@ proc loadProjectConfig*(cfgFile: string): ProjectCfg =
|
|||||||
artifacts: pJson.getIfExists("artifacts").getElems.mapIt(it.getStr),
|
artifacts: pJson.getIfExists("artifacts").getElems.mapIt(it.getStr),
|
||||||
cmdInput: pJson.getIfExists("cmdInput").getElems.mapIt(it.getStr),
|
cmdInput: pJson.getIfExists("cmdInput").getElems.mapIt(it.getStr),
|
||||||
expectedEnv: pJson.getIfExists("expectedEnv").getElems.mapIt(it.getStr),
|
expectedEnv: pJson.getIfExists("expectedEnv").getElems.mapIt(it.getStr),
|
||||||
dontSkip: pJson.getIfExists("dontSkip").getStr("false") != "false")
|
dontSkip: pJson.getIfExists("dontSkip").getBVal(false))
|
||||||
|
|
||||||
if steps[sName].stepCmd == "sh" and steps[sName].cmdInput.len == 0:
|
if steps[sName].stepCmd == "sh" and steps[sName].cmdInput.len == 0:
|
||||||
warn "Step " & sName & " uses 'sh' as its command but has no cmdInput."
|
warn "Step " & sName & " uses 'sh' as its command but has no cmdInput."
|
||||||
|
|
||||||
result = ProjectCfg(
|
result = ProjectConfig(
|
||||||
name: jsonCfg.getOrFail("name", "project configuration").getStr,
|
name: jsonCfg.getOrFail("name", "project configuration").getStr,
|
||||||
versionCmd: jsonCfg.getIfExists("versionCmd").getStr("git describe --tags --always"),
|
versionCmd: jsonCfg.getIfExists("versionCmd").getStr("git describe --tags --always"),
|
||||||
steps: steps)
|
steps: steps)
|
||||||
@ -121,6 +137,7 @@ proc loadBuildStatus*(statusFile: string): BuildStatus =
|
|||||||
state: jsonObj.getOrFail("state", "build status").getStr,
|
state: jsonObj.getOrFail("state", "build status").getStr,
|
||||||
details: jsonObj.getIfExists("details").getStr("") )
|
details: jsonObj.getIfExists("details").getStr("") )
|
||||||
|
|
||||||
|
# TODO: unused and untested, add tests if we start using this
|
||||||
proc parseRunRequest*(reqStr: string): RunRequest =
|
proc parseRunRequest*(reqStr: string): RunRequest =
|
||||||
let reqJson = parseJson(reqStr)
|
let reqJson = parseJson(reqStr)
|
||||||
|
|
@ -13,7 +13,7 @@ type
|
|||||||
env*: StringTableRef ## environment variables for all build processes
|
env*: StringTableRef ## environment variables for all build processes
|
||||||
openedFiles*: seq[File] ## all files that we have opened that need to be closed
|
openedFiles*: seq[File] ## all files that we have opened that need to be closed
|
||||||
outputHandler*: HandleProcMsgCB ## handler for process output
|
outputHandler*: HandleProcMsgCB ## handler for process output
|
||||||
project*: ProjectCfg ## the project configuration
|
project*: ProjectConfig ## the project configuration
|
||||||
projectDef*: ProjectDef ## the StrawBoss project definition
|
projectDef*: ProjectDef ## the StrawBoss project definition
|
||||||
status*: BuildStatus ## the current status of the build
|
status*: BuildStatus ## the current status of the build
|
||||||
statusFile*: string ## absolute path to the build status file
|
statusFile*: string ## absolute path to the build status file
|
||||||
@ -188,7 +188,7 @@ proc runStep*(cfg: StrawBossConfig, req: RunRequest,
|
|||||||
env: env,
|
env: env,
|
||||||
openedFiles: @[stdoutFile, stderrFile],
|
openedFiles: @[stdoutFile, stderrFile],
|
||||||
outputHandler: combineProcMsgHandlers(outputHandler, logFilesOH),
|
outputHandler: combineProcMsgHandlers(outputHandler, logFilesOH),
|
||||||
project: ProjectCfg(),
|
project: ProjectConfig(),
|
||||||
projectDef: matching[0],
|
projectDef: matching[0],
|
||||||
status: result,
|
status: result,
|
||||||
statusFile: req.workspaceDir & "/" & "status.json",
|
statusFile: req.workspaceDir & "/" & "status.json",
|
@ -1,4 +1,4 @@
|
|||||||
import asyncdispatch, jester, json, jwt, osproc, sequtils, tempfile, times
|
import asyncdispatch, bcrypt, jester, json, jwt, osproc, sequtils, tempfile, times
|
||||||
|
|
||||||
import ./configuration, ./core, private/util
|
import ./configuration, ./core, private/util
|
||||||
|
|
||||||
@ -15,11 +15,13 @@ type
|
|||||||
issuedAt*, expires*: TimeInfo
|
issuedAt*, expires*: TimeInfo
|
||||||
|
|
||||||
const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
|
const ISO_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
|
||||||
|
const BCRYPT_ROUNDS = 16
|
||||||
|
|
||||||
proc makeJsonResp(status: HttpCode): string =
|
proc makeJsonResp(status: HttpCode, details: string = ""): string =
|
||||||
result = $(%* {
|
result = $(%* {
|
||||||
"statusCode": status.int,
|
"statusCode": status.int,
|
||||||
"status": $status
|
"status": $status,
|
||||||
|
"details": details
|
||||||
})
|
})
|
||||||
|
|
||||||
proc newSession(user: UserRef): Session =
|
proc newSession(user: UserRef): Session =
|
||||||
@ -61,7 +63,7 @@ proc extractSession(cfg: StrawBossConfig, request: Request): Session =
|
|||||||
template requireAuth() =
|
template requireAuth() =
|
||||||
var session {.inject.}: Session
|
var session {.inject.}: Session
|
||||||
try: session = extractSession(givenCfg, request)
|
try: session = extractSession(givenCfg, request)
|
||||||
except: resp Http401, makeJsonResp(Http401), "application/json"
|
except: resp(Http401, makeJsonResp(Http401), "application/json")
|
||||||
|
|
||||||
proc spawnWorker(req: RunRequest): Worker =
|
proc spawnWorker(req: RunRequest): Worker =
|
||||||
let dir = mkdtemp()
|
let dir = mkdtemp()
|
||||||
@ -77,15 +79,36 @@ proc start*(givenCfg: StrawBossConfig): void =
|
|||||||
|
|
||||||
routes:
|
routes:
|
||||||
get "/api/ping":
|
get "/api/ping":
|
||||||
resp $(%*"pong"), "application/json"
|
resp($(%*"pong"), "application/json")
|
||||||
|
|
||||||
get "/api/auth-token":
|
get "/api/auth-token":
|
||||||
resp Http501, makeJsonResp(Http501), "application/json"
|
resp(Http501, makeJsonResp(Http501), "application/json")
|
||||||
|
|
||||||
get "/api/projects":
|
get "/api/projects":
|
||||||
requireAuth()
|
requireAuth()
|
||||||
#let projectDefs: seq[ProjectDef] = givenCfg.projects.mapIt(it)
|
resp($(%(givenCfg.projects)), "application/json")
|
||||||
resp $(%(givenCfg.projects)), "application/json"
|
|
||||||
|
get "/api/auth-token":
|
||||||
|
var username, pwd: string
|
||||||
|
try:
|
||||||
|
username = @"username"
|
||||||
|
pwd = @"password"
|
||||||
|
except: resp(Http401, makeJsonResp(Http401, "fields 'username' and 'password' required"))
|
||||||
|
|
||||||
|
let users = givenCfg.users.filterIt(it.name == username)
|
||||||
|
if users.len != 1:
|
||||||
|
resp(Http401, makeJsonResp(Http401, "invalid username or password"))
|
||||||
|
|
||||||
|
let user = users[0]
|
||||||
|
|
||||||
|
# generate salt
|
||||||
|
let salt = genSalt(BCRYPT_ROUNDS)
|
||||||
|
|
||||||
|
# bcrypt
|
||||||
|
let hashedPwd = hash(pwd, salt)
|
||||||
|
stdout.writeLine "Hashed pwd is " & $hashedPwd
|
||||||
|
|
||||||
|
resp(Http501, makeJsonResp(Http501))
|
||||||
|
|
||||||
post "/api/project/@projectName/@stepName/run/@buildRef?":
|
post "/api/project/@projectName/@stepName/run/@buildRef?":
|
||||||
workers.add(spawnWorker(RunRequest(
|
workers.add(spawnWorker(RunRequest(
|
17
src/test/json/strawboss.config.json
Normal file
17
src/test/json/strawboss.config.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"artifactsRepo": "artifacts",
|
||||||
|
"users": [
|
||||||
|
{ "name": "bob@builder.com", "hashedPwd": "testvalue" },
|
||||||
|
{ "name": "sam@sousa.com", "hashedPwd": "testvalue" }
|
||||||
|
],
|
||||||
|
"authSecret": "change me",
|
||||||
|
"projects": [
|
||||||
|
{ "name": "test-project-1",
|
||||||
|
"repo": "/non-existent/dir",
|
||||||
|
"cfgFilePath": "strawhat.json",
|
||||||
|
"defaultBranch": "deploy",
|
||||||
|
"envVars": { "VAR1": "value" }
|
||||||
|
},
|
||||||
|
{ "name": "test-strawboss",
|
||||||
|
"repo": "https://git.jdb-labs.com:jdb/test-strawboss.git" } ]
|
||||||
|
}
|
16
src/test/json/test-project-1.config.json
Normal file
16
src/test/json/test-project-1.config.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "test-project-1",
|
||||||
|
"versionCmd": "git describe --all --always",
|
||||||
|
"steps": {
|
||||||
|
"build": {
|
||||||
|
"depends": ["test"],
|
||||||
|
"workingDir": "dir1",
|
||||||
|
"stepCmd": "cust-build",
|
||||||
|
"artifacts": ["bin1", "doc1"],
|
||||||
|
"expectedEnv": ["VAR1"],
|
||||||
|
"dontSkip": true,
|
||||||
|
"cmdInput": ["test", "this"]
|
||||||
|
},
|
||||||
|
"test": { }
|
||||||
|
}
|
||||||
|
}
|
4
src/test/json/test-status.json
Normal file
4
src/test/json/test-status.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"state": "failed",
|
||||||
|
"details": "some very good reason"
|
||||||
|
}
|
BIN
src/test/nim/strawbosspkg/tconfiguration
Normal file
BIN
src/test/nim/strawbosspkg/tconfiguration
Normal file
Binary file not shown.
62
src/test/nim/strawbosspkg/tconfiguration.nim
Normal file
62
src/test/nim/strawbosspkg/tconfiguration.nim
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import strtabs, tables, unittest
|
||||||
|
import ./testutil
|
||||||
|
import ../../../main/nim/strawbosspkg/configuration
|
||||||
|
|
||||||
|
suite "load and save configuration objects":
|
||||||
|
|
||||||
|
test "loadStrawBossConfig":
|
||||||
|
let cfg = loadStrawBossConfig("src/test/json/strawboss.config.json")
|
||||||
|
let expectedUsers = @[UserRef(name: "bob@builder.com", hashedPwd: "testvalue"),
|
||||||
|
UserRef(name: "sam@sousa.com", hashedPwd: "testvalue")]
|
||||||
|
let expectedProjects = @[
|
||||||
|
ProjectDef(name: "test-project-1",
|
||||||
|
repo: "/non-existent/dir",
|
||||||
|
defaultBranch: "deploy",
|
||||||
|
cfgFilePath: "strawhat.json",
|
||||||
|
envVars: newStringTable("VAR1", "value", modeCaseSensitive)),
|
||||||
|
ProjectDef(name: "test-strawboss",
|
||||||
|
repo: "https://git.jdb-labs.com:jdb/test-strawboss.git",
|
||||||
|
defaultBranch: "master",
|
||||||
|
cfgFilePath: "strawboss.json",
|
||||||
|
envVars: newStringTable(modeCaseSensitive))]
|
||||||
|
|
||||||
|
check:
|
||||||
|
cfg.artifactsRepo == "artifacts"
|
||||||
|
cfg.authSecret == "change me"
|
||||||
|
sameContents(expectedUsers, cfg.users)
|
||||||
|
sameContents(expectedProjects, cfg.projects)
|
||||||
|
|
||||||
|
test "loadProjectConfig":
|
||||||
|
let pc = loadProjectConfig("src/test/json/test-project-1.config.json")
|
||||||
|
|
||||||
|
check:
|
||||||
|
pc.name == "test-project-1"
|
||||||
|
pc.versionCmd == "git describe --all --always"
|
||||||
|
pc.steps.len == 2
|
||||||
|
|
||||||
|
# Explicitly set properties
|
||||||
|
pc.steps["build"].name == "build"
|
||||||
|
pc.steps["build"].dontSkip == true
|
||||||
|
pc.steps["build"].stepCmd == "cust-build"
|
||||||
|
pc.steps["build"].workingDir == "dir1"
|
||||||
|
sameContents(pc.steps["build"].artifacts, @["bin1", "doc1"])
|
||||||
|
sameContents(pc.steps["build"].depends, @["test"])
|
||||||
|
sameContents(pc.steps["build"].expectedEnv, @["VAR1"])
|
||||||
|
sameContents(pc.steps["build"].cmdInput, @["test", "this"])
|
||||||
|
|
||||||
|
# Step with defaulted properties
|
||||||
|
pc.steps["test"].name == "test"
|
||||||
|
pc.steps["test"].dontSkip == false
|
||||||
|
pc.steps["test"].stepCmd == "sh"
|
||||||
|
pc.steps["test"].workingDir == "."
|
||||||
|
sameContents(pc.steps["test"].artifacts, @[])
|
||||||
|
sameContents(pc.steps["test"].depends, @[])
|
||||||
|
sameContents(pc.steps["test"].expectedEnv, @[])
|
||||||
|
sameContents(pc.steps["test"].cmdInput, @[])
|
||||||
|
|
||||||
|
test "loadBuildStatus":
|
||||||
|
let st = loadBuildStatus("src/test/json/test-status.json")
|
||||||
|
|
||||||
|
check:
|
||||||
|
st.state == "failed"
|
||||||
|
st.details == "some very good reason"
|
52
src/test/nim/strawbosspkg/testutil.nim
Normal file
52
src/test/nim/strawbosspkg/testutil.nim
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import unittest, sequtils
|
||||||
|
|
||||||
|
proc sameContents*[T](a1, a2: openArray[T]): bool =
|
||||||
|
# Answers the question: do these two arrays contain the same contents,
|
||||||
|
# regardless of their order?
|
||||||
|
if a1.len != a2.len: return false
|
||||||
|
for a in a1:
|
||||||
|
if not a2.anyIt(a == it): return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
type
|
||||||
|
SimpleObj* = object
|
||||||
|
strVal*: string
|
||||||
|
|
||||||
|
TestObj = object
|
||||||
|
strVal*: string
|
||||||
|
intVal*: int
|
||||||
|
objVal*: SimpleObj
|
||||||
|
|
||||||
|
suite "testutil":
|
||||||
|
|
||||||
|
test "sameContents":
|
||||||
|
let a1 = [1, 2, 3, 4]
|
||||||
|
let a2 = [3, 2, 4, 1]
|
||||||
|
let a3 = [1, 2, 3, 5]
|
||||||
|
let b1 = ["this", "is", "a", "test"]
|
||||||
|
let b2 = ["a", "test", "this", "is"]
|
||||||
|
let b3 = ["a", "negative", "test", "this", "is"]
|
||||||
|
let c1 = [TestObj(strVal: "a", intVal: 1, objVal: SimpleObj(strVal: "innerA")),
|
||||||
|
TestObj(strVal: "b", intVal: 2, objVal: SimpleObj(strVal: "innerB"))]
|
||||||
|
let c2 = [c1[1], c1[0]]
|
||||||
|
|
||||||
|
check:
|
||||||
|
sameContents(a1, a2)
|
||||||
|
sameContents(b2, b1)
|
||||||
|
sameContents(c1, c2)
|
||||||
|
sameContents(a1, a3) == false
|
||||||
|
sameContents(b1, b3) == false
|
||||||
|
|
||||||
|
test "sameContents (seq)":
|
||||||
|
let a1 = @[1, 2, 3, 4]
|
||||||
|
let a2 = @[3, 2, 4, 1]
|
||||||
|
|
||||||
|
check:
|
||||||
|
sameContents(a1, a2)
|
||||||
|
|
||||||
|
test "sameContents (cross-type)":
|
||||||
|
let a1 = @[1, 2, 3, 4]
|
||||||
|
let a2 = [3, 2, 4, 1]
|
||||||
|
|
||||||
|
check:
|
||||||
|
sameContents(a1, a2)
|
@ -9,6 +9,6 @@ srcDir = "src/main/nim"
|
|||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
requires @["nim >= 0.16.1", "docopt >= 0.1.0", "tempfile", "jester"]
|
requires @["nim >= 0.16.1", "docopt >= 0.1.0", "tempfile", "jester", "bcrypt"]
|
||||||
requires "https://github.com/yglukhov/nim-jwt"
|
requires "https://github.com/yglukhov/nim-jwt"
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user