package api import ( "encoding/json" "net/http" "testing" ) func TestStaffCanRegisterACustomerNobodyHasPhotographed(t *testing.T) { s, fs := newServer(t) seedUser(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", "/api/customers", sess.Token, map[string]string{ "full_name": "Asha Menon", "phone": "9876543210", }) if rec.Code != http.StatusCreated { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } var c Customer if err := json.Unmarshal(rec.Body.Bytes(), &c); err != nil { t.Fatal(err) } // A reference a person can say, from the same counter the engine uses. if c.Ref == "" || c.Label != "Asha Menon" { t.Errorf("got ref=%q label=%q, want a V- reference and the typed name", c.Ref, c.Label) } } // A record with no name and no phone is a number nobody can search for, and // the customer at the counter is the only source of either. func TestACustomerNeedsANameOrAPhone(t *testing.T) { s, fs := newServer(t) seedUser(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", "/api/customers", sess.Token, map[string]string{"notes": "regular, likes the window seat"}) if rec.Code != http.StatusBadRequest { t.Errorf("got %d, want 400: %s", rec.Code, rec.Body.String()) } } // ---------------------------------------------------------------- merge // Uuid-shaped on purpose: resolveVisitor takes a uuid or a V- reference and // correctly refuses anything else, so a made-up id would 404 before reaching // the handler under test. const ( vTyped = "aaaaaaaa-1111-4111-8111-aaaaaaaaaaaa" // typed in at the counter vSeen = "bbbbbbbb-2222-4222-8222-bbbbbbbbbbbb" // enrolled by a camera ) func seedTwoCustomers(fs *fakeStore) { seedUser(fs) fs.visitors = []Customer{ {ID: vTyped, Ref: "V-1", Label: "Asha Menon", FullName: "Asha Menon"}, {ID: vSeen, Ref: "V-2", Label: "Visitor 2"}, } } func TestMergingFoldsOneCustomerIntoTheOtherAndSaysWhatMoved(t *testing.T) { s, fs := newServer(t) seedTwoCustomers(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", "/api/visitors/"+vTyped+"/merge", sess.Token, map[string]string{"into": vSeen}) if rec.Code != http.StatusOK { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } var out MergeResult if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { t.Fatal(err) } // The reference that STOPPED resolving has to be named. Staff write these // on cards; discovering it at a counter is the wrong place to find out. if out.RetiredRef != "V-1" || out.Ref != "V-2" { t.Errorf("kept %q retired %q, want V-2 kept and V-1 retired", out.Ref, out.RetiredRef) } } // The only irreversible operation on a customer apart from erasure. Two people // welded together cannot be separated: nothing records which visit came from // whom. func TestStaffCannotMerge(t *testing.T) { s, fs := newServer(t) seedTwoCustomers(fs) fs.addUser("shopfloor@acme.com", "correct horse battery", UserRecord{ ID: "u9", ClientID: "client-acme", Role: "staff", Active: true, }) sess := login(t, s, "shopfloor@acme.com", "correct horse battery") rec := do(t, s, "POST", "/api/visitors/"+vTyped+"/merge", sess.Token, map[string]string{"into": vSeen}) if rec.Code != http.StatusForbidden { t.Errorf("got %d, want 403 for staff: %s", rec.Code, rec.Body.String()) } } func TestMergingACustomerIntoThemselvesIsRefused(t *testing.T) { s, fs := newServer(t) seedTwoCustomers(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", "/api/visitors/"+vTyped+"/merge", sess.Token, map[string]string{"into": vTyped}) if rec.Code != http.StatusBadRequest { t.Errorf("got %d, want 400: %s", rec.Code, rec.Body.String()) } } func TestMergingNeedsATarget(t *testing.T) { s, fs := newServer(t) seedTwoCustomers(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") for _, body := range []map[string]string{{}, {"into": " "}, {"into": "V-999"}} { rec := do(t, s, "POST", "/api/visitors/"+vTyped+"/merge", sess.Token, body) if rec.Code == http.StatusOK { t.Errorf("merge with %v succeeded, want a refusal", body) } } } // Every merge leaves a trace: it is destructive and cannot be undone. func TestAMergeIsAudited(t *testing.T) { s, fs := newServer(t) seedTwoCustomers(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") do(t, s, "POST", "/api/visitors/"+vTyped+"/merge", sess.Token, map[string]string{"into": vSeen}) found := false for _, a := range fs.audits { if a.Action == "customer.merge" { found = true if a.Detail["retired_ref"] != "V-1" { t.Errorf("audit must name the retired reference: %+v", a.Detail) } } } if !found { t.Error("no audit row for a merge") } }