package api import ( "encoding/json" "net/http" "strings" "testing" ) func seedSite(fs *fakeStore) { fs.sites = []SiteHealth{{SiteID: siteA, Slug: "chennai", Name: "TeNext Chennai"}} } func TestAManagerCanGetACodeForTheirOwnShop(t *testing.T) { s, fs := newServer(t) seedUser(fs) seedSite(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", "/api/sites/"+siteA+"/enrolment-code", sess.Token, map[string]any{"label": "counter PC"}) if rec.Code != http.StatusCreated { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } var out EnrolmentCode if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { t.Fatal(err) } if out.Code == "" || out.ExpiresAt.IsZero() { t.Fatalf("no usable code came back: %s", rec.Body.String()) } // Grouped for reading aloud - the installer is on the phone. if !strings.Contains(out.Code, "-") { t.Errorf("code is not grouped for dictation: %q", out.Code) } if out.SiteName != "TeNext Chennai" { t.Errorf("the shop is not named back to the operator: %q", out.SiteName) } } // Staff must not be able to mint one. The code is redeemed for the site's // broker password, so it is a credential and not a convenience - and an // instruction in a prompt or a hidden button is not a permission check. func TestStaffCannotMintAnEnrolmentCode(t *testing.T) { s, fs := newServer(t) seedSite(fs) fs.addUser("staff@acme.com", "correct horse battery", UserRecord{ ID: "u2", ClientID: "client-acme", Role: "staff", Active: true, }) sess := login(t, s, "staff@acme.com", "correct horse battery") rec := do(t, s, "POST", "/api/sites/"+siteA+"/enrolment-code", sess.Token, nil) if rec.Code != http.StatusForbidden { t.Fatalf("staff minted a credential: %d %s", rec.Code, rec.Body.String()) } } // A shop belonging to somebody else is NOT FOUND, not forbidden: a tenant has // no business learning that another tenant's shop exists. func TestAnotherTenantsShopIsNotFound(t *testing.T) { s, fs := newServer(t) seedUser(fs) seedSite(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") other := "bbbbbbbb-1111-2222-3333-444444444444" rec := do(t, s, "POST", "/api/sites/"+other+"/enrolment-code", sess.Token, nil) if rec.Code != http.StatusNotFound { t.Fatalf("got %d, want 404: %s", rec.Code, rec.Body.String()) } } // A code is read aloud, photographed and pasted into chat on its way to a // shop. A caller asking for a year of validity gets a month. func TestCodeLifetimeIsCapped(t *testing.T) { s, fs := newServer(t) seedUser(fs) seedSite(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") do(t, s, "POST", "/api/sites/"+siteA+"/enrolment-code", sess.Token, map[string]any{"days": 3650}) if got := fs.lastCodeTTL.Hours(); got > 30*24 { t.Fatalf("ttl was %v, want at most 30 days", fs.lastCodeTTL) } // And a code records who minted it - it hands out a broker password. if fs.lastCodeActor == "" { t.Error("the code was not attributed to the person who asked for it") } }