250 lines
8.7 KiB
Go
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
|
|
}
|