Files
box-manifest-android/server/internal/httpapi/handler_test.go
T

250 lines
8.7 KiB
Go

package httpapi
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"box-manifest/server/internal/tree"
_ "modernc.org/sqlite"
)
func TestNodeLifecycleThroughHTTP(t *testing.T) {
handler := newTestHandler(t)
create := request(t, handler, http.MethodPost, "/api/nodes", `{"name":"Pens","quantity":12}`)
if create.Code != http.StatusCreated {
t.Fatalf("create: got %d: %s", create.Code, create.Body.String())
}
var node tree.Node
if err := json.Unmarshal(create.Body.Bytes(), &node); err != nil {
t.Fatal(err)
}
if node.ParentID == nil || *node.ParentID != tree.UnsortedID {
t.Fatalf("expected default Unsorted parent, got %#v", node)
}
if node.LookupCode == "" {
t.Fatal("created node has no lookup code")
}
lookup := request(t, handler, http.MethodGet, "/api/nodes/by-code/"+node.LookupCode, "")
if lookup.Code != http.StatusOK || !bytes.Contains(lookup.Body.Bytes(), []byte(node.ID)) {
t.Fatalf("lookup: got %d: %s", lookup.Code, lookup.Body.String())
}
canonical := request(t, handler, http.MethodGet, "/n/"+node.LookupCode, "")
if canonical.Code != http.StatusSeeOther || canonical.Header().Get("Location") != "/?code="+node.LookupCode {
t.Fatalf("canonical route: got %d, %q", canonical.Code, canonical.Header().Get("Location"))
}
update := request(t, handler, http.MethodPatch, "/api/nodes/"+node.ID, `{"quantity":null}`)
if update.Code != http.StatusOK {
t.Fatalf("update: got %d: %s", update.Code, update.Body.String())
}
if err := json.Unmarshal(update.Body.Bytes(), &node); err != nil {
t.Fatal(err)
}
if node.Quantity != nil {
t.Fatalf("expected quantity to be cleared, got %v", *node.Quantity)
}
treeResponse := request(t, handler, http.MethodGet, "/api/tree", "")
if treeResponse.Code != http.StatusOK {
t.Fatalf("tree: got %d: %s", treeResponse.Code, treeResponse.Body.String())
}
deleteResponse := request(t, handler, http.MethodDelete, "/api/nodes/"+node.ID, "")
if deleteResponse.Code != http.StatusNoContent {
t.Fatalf("delete: got %d: %s", deleteResponse.Code, deleteResponse.Body.String())
}
}
func TestBulkCreateThroughHTTP(t *testing.T) {
handler := newTestHandler(t)
response := request(
t,
handler,
http.MethodPost,
"/api/nodes/bulk",
`{"parentId":"unsorted","names":["Hammer","Screws"]}`,
)
if response.Code != http.StatusCreated {
t.Fatalf("bulk create: got %d: %s", response.Code, response.Body.String())
}
var body struct {
Nodes []tree.Node `json:"nodes"`
}
if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if len(body.Nodes) != 2 || body.Nodes[0].Name != "Hammer" || body.Nodes[1].Name != "Screws" {
t.Fatalf("unexpected response: %#v", body.Nodes)
}
}
func TestBulkMoveAndDeleteThroughHTTP(t *testing.T) {
handler := newTestHandler(t)
first := createNodeThroughHTTP(t, handler, tree.UnsortedID, "first")
second := createNodeThroughHTTP(t, handler, tree.UnsortedID, "second")
target := createNodeThroughHTTP(t, handler, tree.RootID, "target")
moveBody := `{"nodeIds":["` + first.ID + `","` + second.ID + `"],"targetParentId":"` + target.ID + `","childHandling":"WITH_SUBTREE"}`
moveResponse := request(t, handler, http.MethodPost, "/api/nodes/bulk/move", moveBody)
if moveResponse.Code != http.StatusOK {
t.Fatalf("bulk move: got %d: %s", moveResponse.Code, moveResponse.Body.String())
}
deleteBody := `{"nodeIds":["` + first.ID + `","` + second.ID + `"]}`
deleteResponse := request(t, handler, http.MethodPost, "/api/nodes/bulk/delete", deleteBody)
if deleteResponse.Code != http.StatusNoContent {
t.Fatalf("bulk delete: got %d: %s", deleteResponse.Code, deleteResponse.Body.String())
}
}
func TestIndividualizeAndCombineThroughHTTP(t *testing.T) {
handler := newTestHandler(t)
create := request(t, handler, http.MethodPost, "/api/nodes", `{"parentId":"unsorted","name":"Bin","quantity":2}`)
var original tree.Node
if err := json.Unmarshal(create.Body.Bytes(), &original); err != nil {
t.Fatal(err)
}
individualize := request(t, handler, http.MethodPost, "/api/nodes/"+original.ID+"/individualize", `{"names":["Bin 01","Bin 02"]}`)
if individualize.Code != http.StatusOK {
t.Fatalf("individualize: got %d: %s", individualize.Code, individualize.Body.String())
}
var body struct {
Nodes []tree.Node `json:"nodes"`
}
if err := json.Unmarshal(individualize.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if len(body.Nodes) != 2 || body.Nodes[0].ID != original.ID {
t.Fatalf("unexpected units: %#v", body.Nodes)
}
combineBody, _ := json.Marshal(map[string]any{"nodeIds": []string{body.Nodes[0].ID, body.Nodes[1].ID}, "retainedNodeId": body.Nodes[0].ID, "name": "Bins"})
combine := request(t, handler, http.MethodPost, "/api/nodes/bulk/combine", string(combineBody))
if combine.Code != http.StatusOK {
t.Fatalf("combine: got %d: %s", combine.Code, combine.Body.String())
}
var combined tree.Node
if err := json.Unmarshal(combine.Body.Bytes(), &combined); err != nil {
t.Fatal(err)
}
if combined.Quantity == nil || *combined.Quantity != 2 || combined.LookupCode != original.LookupCode {
t.Fatalf("unexpected combined node: %#v", combined)
}
}
func TestTypesTagsAndClassificationThroughHTTP(t *testing.T) {
handler := newTestHandler(t)
node := createNodeThroughHTTP(t, handler, tree.UnsortedID, "Box")
typeResponse := request(t, handler, http.MethodPost, "/api/node-types", `{"name":"Storage bin","color":"#377aff"}`)
if typeResponse.Code != http.StatusCreated {
t.Fatalf("create type: %d %s", typeResponse.Code, typeResponse.Body.String())
}
var nodeType tree.NodeType
if err := json.Unmarshal(typeResponse.Body.Bytes(), &nodeType); err != nil {
t.Fatal(err)
}
tagResponse := request(t, handler, http.MethodPost, "/api/tags", `{"name":"Blue"}`)
if tagResponse.Code != http.StatusCreated {
t.Fatalf("create tag: %d %s", tagResponse.Code, tagResponse.Body.String())
}
var tag tree.Tag
if err := json.Unmarshal(tagResponse.Body.Bytes(), &tag); err != nil {
t.Fatal(err)
}
body, _ := json.Marshal(map[string]any{"typeId": nodeType.ID, "tagIds": []string{tag.ID}})
classified := request(t, handler, http.MethodPut, "/api/nodes/"+node.ID+"/classification", string(body))
if classified.Code != http.StatusOK {
t.Fatalf("classify: %d %s", classified.Code, classified.Body.String())
}
var result tree.Node
if err := json.Unmarshal(classified.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
if result.TypeID == nil || *result.TypeID != nodeType.ID || len(result.TagIDs) != 1 {
t.Fatalf("classification missing: %#v", result)
}
}
func TestTreeImportPreviewAndCommitThroughHTTP(t *testing.T) {
handler := newTestHandler(t)
preview := request(
t,
handler,
http.MethodPost,
"/api/imports/tree/preview",
`{"parentId":"unsorted","manifest":{"nodes":[{"name":"Box A","children":[{"name":"Cable","quantity":2}]}]}}`,
)
if preview.Code != http.StatusOK {
t.Fatalf("preview: got %d: %s", preview.Code, preview.Body.String())
}
var previewBody struct {
PlanID string `json:"planId"`
}
if err := json.Unmarshal(preview.Body.Bytes(), &previewBody); err != nil {
t.Fatal(err)
}
commit := request(
t,
handler,
http.MethodPost,
"/api/imports/tree/commit",
`{"planId":"`+previewBody.PlanID+`"}`,
)
if commit.Code != http.StatusCreated {
t.Fatalf("commit: got %d: %s", commit.Code, commit.Body.String())
}
treeResponse := request(t, handler, http.MethodGet, "/api/tree", "")
if !bytes.Contains(treeResponse.Body.Bytes(), []byte(`"name":"Box A"`)) ||
!bytes.Contains(treeResponse.Body.Bytes(), []byte(`"name":"Cable"`)) {
t.Fatalf("imported tree is missing: %s", treeResponse.Body.String())
}
}
func newTestHandler(t *testing.T) http.Handler {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
store := tree.NewStore(db)
if err := store.Initialize(context.Background()); err != nil {
t.Fatal(err)
}
return New(store)
}
func request(t *testing.T, handler http.Handler, method, path, body string) *httptest.ResponseRecorder {
t.Helper()
request := httptest.NewRequest(method, path, bytes.NewBufferString(body))
if body != "" {
request.Header.Set("Content-Type", "application/json")
}
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
return response
}
func createNodeThroughHTTP(t *testing.T, handler http.Handler, parentID, name string) tree.Node {
t.Helper()
body, err := json.Marshal(map[string]any{"parentId": parentID, "name": name})
if err != nil {
t.Fatal(err)
}
response := request(t, handler, http.MethodPost, "/api/nodes", string(body))
if response.Code != http.StatusCreated {
t.Fatalf("create %s: got %d: %s", name, response.Code, response.Body.String())
}
var node tree.Node
if err := json.Unmarshal(response.Body.Bytes(), &node); err != nil {
t.Fatal(err)
}
return node
}