package connector import ( "context" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/missionworkforce/missionmatch/internal/platform/sourcing" ) // fakeICIMS is a minimal iCIMS API stand in: it issues a bearer token and serves two candidate pages, counting // token fetches so the test can assert the token is cached. type fakeICIMS struct { tokenFetches int throttle bool lastSince string } func (f *fakeICIMS) handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) { f.tokenFetches++ _ = r.ParseForm() if r.FormValue("grant_type") != "client_credentials" { http.Error(w, "bad grant", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"access_token":"[credential removed]","expires_in":3600,"token_type":"Bearer"}`)) }) mux.HandleFunc("/customers/CUST1/search/people", func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer tok-abc" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } if f.throttle { http.Error(w, "slow down", http.StatusTooManyRequests) return } f.lastSince = r.URL.Query().Get("updatedSince") w.Header().Set("Content-Type", "application/json") switch r.URL.Query().Get("page") { case "": // first page, one good record plus one incomplete record that must be skipped _, _ = w.Write([]byte(`{"people":[ {"id":"1","name":"Ada Lovelace","title":"Engineer","location":"Honolulu HI","skills":["rmf","cloud security"],"years":7,"email":"ada@example.com","updated":"2026-02-01T00:00:00Z"}, {"id":"","name":"No Id","updated":"2026-02-05T00:00:00Z"} ],"nextPage":"p2"}`)) case "p2": // last page _, _ = w.Write([]byte(`{"people":[ {"id":"2","name":"Grace Hopper","title":"Architect","location":"Santa Fe NM","skills":["ato"],"years":12,"email":"grace@example.com","updated":"2026-03-10T00:00:00Z"} ],"nextPage":""}`)) } }) return mux } func TestICIMSFetchPageMapsAndPaginates(t *testing.T) { f := &fakeICIMS{} srv := httptest.NewServer(f.handler()) defer srv.Close() c := NewICIMS("CUST1", "cid", "secret", srv.URL, srv.Client()) ctx := context.Background() since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) // First page: the good record maps, the id less record is skipped, nextPage is p2, maxUpdated is the good // record's timestamp (the skipped record's later timestamp must not count). people, next, maxUpdated, err := c.FetchPage(ctx, since, "") if err != nil { t.Fatalf("fetch page 1: %v", err) } if len(people) != 1 { t.Fatalf("the id less record must be skipped, got %d people", len(people)) } if people[0].ExternalID != "1" || people[0].FullName != "Ada Lovelace" || people[0].Years != 7 { t.Fatalf("mapping wrong: %+v", people[0]) } if len(people[0].Emails) != 1 || people[0].Emails[0] != "ada@example.com" { t.Fatalf("email must map, got %v", people[0].Emails) } if next != "p2" { t.Fatalf("next page must be p2, got %q", next) } if !maxUpdated.Equal(time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)) { t.Fatalf("maxUpdated must be the good record's timestamp, got %v", maxUpdated) } // The incremental window was passed as updatedSince. if !strings.HasPrefix(f.lastSince, "2026-01-01") { t.Fatalf("updatedSince must carry the watermark, got %q", f.lastSince) } // Second page: the last page, nextPage empty. people2, next2, _, err := c.FetchPage(ctx, since, next) if err != nil { t.Fatalf("fetch page 2: %v", err) } if len(people2) != 1 || people2[0].ExternalID != "2" { t.Fatalf("page 2 mapping wrong: %+v", people2) } if next2 != "" { t.Fatalf("the last page must report no next page, got %q", next2) } // The token was fetched once and cached across both pages. if f.tokenFetches != 1 { t.Fatalf("the bearer token must be fetched once and cached, got %d fetches", f.tokenFetches) } } func TestICIMSThrottleMapsToErrThrottled(t *testing.T) { f := &fakeICIMS{throttle: true} srv := httptest.NewServer(f.handler()) defer srv.Close() c := NewICIMS("CUST1", "cid", "secret", srv.URL, srv.Client()) _, _, _, err := c.FetchPage(context.Background(), time.Unix(0, 0), "") if !errors.Is(err, sourcing.ErrThrottled) { t.Fatalf("a 429 must map to ErrThrottled, got %v", err) } } func TestICIMSName(t *testing.T) { if NewICIMS("c", "i", "s", "", nil).Name() != "icims" { t.Fatal("connector name must be icims") } }