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

183 lines
5.9 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 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
}