feat: add sqlite-backed API docs and tests
All checks were successful
ci / test-and-build (push) Successful in 11m21s
All checks were successful
ci / test-and-build (push) Successful in 11m21s
This commit is contained in:
140
internal/api/server_test.go
Normal file
140
internal/api/server_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"work-queue-api/internal/db"
|
||||
)
|
||||
|
||||
func newTestServer(t *testing.T) (*Server, *sql.DB) {
|
||||
t.Helper()
|
||||
sqliteDB, err := db.Open(t.TempDir() + "/test.db")
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
server, err := NewServer(sqliteDB)
|
||||
if err != nil {
|
||||
t.Fatalf("new server: %v", err)
|
||||
}
|
||||
return server, sqliteDB
|
||||
}
|
||||
|
||||
func doJSON(t *testing.T, h http.Handler, method, path string, body any) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
var payload []byte
|
||||
if body != nil {
|
||||
var err error
|
||||
payload, err = json.Marshal(body)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal body: %v", err)
|
||||
}
|
||||
}
|
||||
req := httptest.NewRequest(method, path, bytes.NewReader(payload))
|
||||
res := httptest.NewRecorder()
|
||||
h.ServeHTTP(res, req)
|
||||
return res
|
||||
}
|
||||
|
||||
func TestProjectAndWorkLifecycle(t *testing.T) {
|
||||
server, sqliteDB := newTestServer(t)
|
||||
defer sqliteDB.Close()
|
||||
router := server.Router()
|
||||
|
||||
projectRes := doJSON(t, router, http.MethodPost, "/projects", map[string]any{
|
||||
"name": "Shopping List API",
|
||||
"external_ref": "todoist:123",
|
||||
})
|
||||
if projectRes.Code != http.StatusCreated {
|
||||
t.Fatalf("create project status = %d body=%s", projectRes.Code, projectRes.Body.String())
|
||||
}
|
||||
var project map[string]any
|
||||
if err := json.Unmarshal(projectRes.Body.Bytes(), &project); err != nil {
|
||||
t.Fatalf("decode project: %v", err)
|
||||
}
|
||||
|
||||
workRes := doJSON(t, router, http.MethodPost, "/work", map[string]any{
|
||||
"project_id": project["id"],
|
||||
"type": "code_review",
|
||||
"description": "Review PR #3",
|
||||
"payload": map[string]any{"pr": 3},
|
||||
"priority": 2,
|
||||
"assigned_agent": "steve-w",
|
||||
"created_by": "marcus-a",
|
||||
})
|
||||
if workRes.Code != http.StatusCreated {
|
||||
t.Fatalf("create work status = %d body=%s", workRes.Code, workRes.Body.String())
|
||||
}
|
||||
var work map[string]any
|
||||
if err := json.Unmarshal(workRes.Body.Bytes(), &work); err != nil {
|
||||
t.Fatalf("decode work: %v", err)
|
||||
}
|
||||
|
||||
for _, step := range []map[string]any{
|
||||
{"status": "dispatched", "assigned_agent": "steve-w"},
|
||||
{"status": "in_progress"},
|
||||
{"status": "completed", "outcome": "success", "notes": "done"},
|
||||
} {
|
||||
res := doJSON(t, router, http.MethodPatch, "/work/"+work["id"].(string), step)
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("patch work step=%v status = %d body=%s", step, res.Code, res.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
getRes := doJSON(t, router, http.MethodGet, "/work/"+work["id"].(string), nil)
|
||||
if getRes.Code != http.StatusOK {
|
||||
t.Fatalf("get work status = %d body=%s", getRes.Code, getRes.Body.String())
|
||||
}
|
||||
var fetched map[string]any
|
||||
if err := json.Unmarshal(getRes.Body.Bytes(), &fetched); err != nil {
|
||||
t.Fatalf("decode fetched work: %v", err)
|
||||
}
|
||||
if fetched["status"] != "completed" {
|
||||
t.Fatalf("expected completed status, got %v", fetched["status"])
|
||||
}
|
||||
logs, ok := fetched["dispatch_log"].([]any)
|
||||
if !ok || len(logs) != 1 {
|
||||
t.Fatalf("expected 1 dispatch log, got %v", fetched["dispatch_log"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOneInProgressPerAgent(t *testing.T) {
|
||||
server, sqliteDB := newTestServer(t)
|
||||
defer sqliteDB.Close()
|
||||
router := server.Router()
|
||||
|
||||
create := func(desc string) string {
|
||||
res := doJSON(t, router, http.MethodPost, "/work", map[string]any{
|
||||
"type": "bug_fix",
|
||||
"description": desc,
|
||||
"assigned_agent": "steve-w",
|
||||
})
|
||||
if res.Code != http.StatusCreated {
|
||||
t.Fatalf("create work status = %d body=%s", res.Code, res.Body.String())
|
||||
}
|
||||
var body map[string]any
|
||||
_ = json.Unmarshal(res.Body.Bytes(), &body)
|
||||
return body["id"].(string)
|
||||
}
|
||||
|
||||
first := create("first")
|
||||
second := create("second")
|
||||
for _, id := range []string{first, second} {
|
||||
res := doJSON(t, router, http.MethodPatch, "/work/"+id, map[string]any{"status": "dispatched", "assigned_agent": "steve-w"})
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("dispatch status = %d body=%s", res.Code, res.Body.String())
|
||||
}
|
||||
}
|
||||
res := doJSON(t, router, http.MethodPatch, "/work/"+first, map[string]any{"status": "in_progress"})
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("first in progress status = %d body=%s", res.Code, res.Body.String())
|
||||
}
|
||||
res = doJSON(t, router, http.MethodPatch, "/work/"+second, map[string]any{"status": "in_progress"})
|
||||
if res.Code != http.StatusConflict {
|
||||
t.Fatalf("expected 409, got %d body=%s", res.Code, res.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
//go:embed migrations/001_initial.sql
|
||||
@@ -18,7 +18,7 @@ func Open(databaseURL string) (*sql.DB, error) {
|
||||
return nil, fmt.Errorf("mkdir db dir: %w", err)
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", databaseURL+"?_foreign_keys=on&_busy_timeout=5000")
|
||||
db, err := sql.Open("sqlite", databaseURL+"?_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user