Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/integration/activity_integration_test.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/integration/activity_integration_test.go |
| Original SHA-256 | 3b24fd24e5d249d17a6397aac18b0bd40a1ada9c798df19f2ee57d74ea1616ba |
| Git blob SHA | 4853ed862f64a0493c4df380df3390d6e7157ebc |
| Reading-copy SHA-256 | 3b24fd24e5d249d17a6397aac18b0bd40a1ada9c798df19f2ee57d74ea1616ba |
Every recorded occurrence
These 2 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/integration/activity_integration_test.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/integration/activity_integration_test.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓267 lines · 9.5 KB
//go:build integration
package integration
import (
"testing"
"time"
"github.com/missionworkforce/missionmatch/internal/modules/recruiting"
)
// WARNING. Point MM_APP_DSN and MM_MIGRATOR_DSN at a scratch or staging database, never at production.
// TestActivityLogIsARecordNotAScratchpad. Everything a metric is computed from is fixed at insert.
func TestActivityLogIsARecordNotAScratchpad(t *testing.T) {
svc, ctx, _, cands := newPipeline(t, 1, 1)
a, err := svc.LogActivity(ctx, recruiting.LogActivityInput{
Kind: "call", Outcome: "left_message", CandidateID: cands[0].String(), Body: "Left a mesage"})
if err != nil {
t.Fatalf("log: %v", err)
}
if a.AutoCaptured {
t.Error("a human logged activity came back marked as captured")
}
if a.ActorName == "" {
t.Error("the actor name was not recorded at write time")
}
// The note may be corrected.
fixed, err := svc.CorrectActivityNote(ctx, a.ID, "Left a message")
if err != nil {
t.Fatalf("correct note: %v", err)
}
if fixed.Body != "Left a message" {
t.Errorf("body = %q after correction", fixed.Body)
}
// And nothing else about it changed.
if fixed.Kind != a.Kind || fixed.OccurredAt != a.OccurredAt || fixed.Outcome != a.Outcome {
t.Error("correcting the note changed something other than the note")
}
}
// TestActivityCannotBeLoggedInTheFuture. Pre logging a month of calls would count work that has not
// happened, while backdating yesterday's calls is how recruiters actually work.
func TestActivityCannotBeLoggedInTheFuture(t *testing.T) {
svc, ctx, _, cands := newPipeline(t, 1, 1)
future := time.Now().UTC().Add(48 * time.Hour).Format(time.RFC3339)
if _, err := svc.LogActivity(ctx, recruiting.LogActivityInput{
Kind: "call", CandidateID: cands[0].String(), OccurredAt: future}); err == nil {
t.Fatal("an activity was logged in the future")
}
// Backdating is permitted.
past := time.Now().UTC().Add(-36 * time.Hour).Format(time.RFC3339)
if _, err := svc.LogActivity(ctx, recruiting.LogActivityInput{
Kind: "call", CandidateID: cands[0].String(), OccurredAt: past}); err != nil {
t.Errorf("a backdated activity was refused: %v", err)
}
}
// TestCapturedRowsCannotBeProducedByAHuman. The API has no field for it and the database discards any
// claim, so the only captured rows are the ones the platform wrote itself.
func TestCapturedRowsComeOnlyFromThePlatform(t *testing.T) {
svc, ctx, req, cands := newPipeline(t, 1, 1)
// Submitting an application captures a submission activity.
if _, err := svc.Submit(ctx, req, recruiting.CreateApplicationInput{CandidateID: cands[0].String()}); err != nil {
t.Fatalf("submit: %v", err)
}
// A human logs one too.
if _, err := svc.LogActivity(ctx, recruiting.LogActivityInput{
Kind: "call", Outcome: "connected", CandidateID: cands[0].String(), Body: "Spoke"}); err != nil {
t.Fatalf("log: %v", err)
}
// The default feed shows only what the human did.
logged, err := svc.ListActivities(ctx, recruiting.ActivityFilter{}, 100, 0)
if err != nil {
t.Fatalf("list: %v", err)
}
if logged.Total != 1 {
t.Errorf("the default feed shows %d activities, want 1 (captured rows excluded)", logged.Total)
}
for _, x := range logged.Activities {
if x.AutoCaptured {
t.Error("a captured row appeared in the default feed")
}
}
// Opting in shows both, and the captured one is the submission.
all, err := svc.ListActivities(ctx, recruiting.ActivityFilter{IncludeCaptured: true}, 100, 0)
if err != nil {
t.Fatalf("list with captured: %v", err)
}
if all.Total != 2 {
t.Fatalf("with captured included the feed shows %d, want 2", all.Total)
}
captured := 0
for _, x := range all.Activities {
if x.AutoCaptured {
captured++
if x.Kind != "submission" {
t.Errorf("the captured row is a %q, want submission", x.Kind)
}
}
}
if captured != 1 {
t.Errorf("%d captured rows, want 1", captured)
}
}
// TestActivityNeedsASubject. An activity about nothing is noise, and it would land in a per candidate or
// per requisition rollup as an untraceable increment.
func TestActivityNeedsASubject(t *testing.T) {
svc, ctx, _, _ := newPipeline(t, 1, 1)
if _, err := svc.LogActivity(ctx, recruiting.LogActivityInput{Kind: "call", Body: "About nobody"}); err == nil {
t.Fatal("an activity with no subject was accepted")
}
if _, err := svc.LogActivity(ctx, recruiting.LogActivityInput{Kind: "not_a_kind"}); err == nil {
t.Fatal("an unknown activity kind was accepted")
}
}
// TestActivitySummarySeparatesEffortFromConnects. A hundred dials with two conversations and a hundred
// conversations are different days, and one number reports them identically.
func TestActivitySummarySeparatesEffortFromConnects(t *testing.T) {
svc, ctx, _, cands := newPipeline(t, 1, 1)
cand := cands[0].String()
// Eight attempts, two of which reached someone.
for i := 0; i < 6; i++ {
if _, err := svc.LogActivity(ctx, recruiting.LogActivityInput{
Kind: "call", Outcome: "no_answer", CandidateID: cand}); err != nil {
t.Fatalf("log: %v", err)
}
}
for _, o := range []string{"connected", "replied"} {
if _, err := svc.LogActivity(ctx, recruiting.LogActivityInput{
Kind: "call", Outcome: o, CandidateID: cand}); err != nil {
t.Fatalf("log %s: %v", o, err)
}
}
s, err := svc.ActivitySummary(ctx, "")
if err != nil {
t.Fatalf("summary: %v", err)
}
if s.TotalLogged != 8 {
t.Errorf("total logged = %d, want 8", s.TotalLogged)
}
if s.TotalConnects != 2 {
t.Errorf("connects = %d, want 2", s.TotalConnects)
}
// Two of eight is exactly 25 percent, and the arithmetic is decimal.
if s.ConnectRate != "25.00" {
t.Errorf("connect rate = %q, want 25.00", s.ConnectRate)
}
if len(s.Recruiters) != 1 {
t.Fatalf("%d recruiters in the summary, want 1", len(s.Recruiters))
}
if s.Recruiters[0].Logged != 8 || s.Recruiters[0].Connects != 2 || s.Recruiters[0].ConnectRate != "25.00" {
t.Errorf("recruiter row = %+v", s.Recruiters[0])
}
// Captured rows must never be pooled into logged effort.
if s.TotalCaptured != 0 {
t.Errorf("captured = %d, want 0 with no platform activity", s.TotalCaptured)
}
// The kind breakdown covers logged effort only.
if len(s.ByKind) != 1 || s.ByKind[0].Kind != "call" || s.ByKind[0].Count != 8 {
t.Errorf("by kind = %+v", s.ByKind)
}
}
// TestSummaryConnectRateAbsentOnNoAttempts. A zero rate on nobody dialling claims a failure that did not
// happen.
func TestSummaryConnectRateAbsentOnNoAttempts(t *testing.T) {
svc, ctx, _, _ := newPipeline(t, 1, 1)
s, err := svc.ActivitySummary(ctx, "")
if err != nil {
t.Fatalf("summary: %v", err)
}
if s.ConnectRate != "" {
t.Errorf("connect rate = %q on an empty window, want it absent", s.ConnectRate)
}
if s.Recruiters == nil || s.ByKind == nil {
t.Error("summary slices must be empty, never nil, so the JSON is [] and not null")
}
}
// TestActivityHistoryOutlivesTheRecruiter. Offboarding must not erase the record of work done, which is
// exactly what Phase 17 derives its metrics from.
func TestActivityHistoryOutlivesTheRecruiter(t *testing.T) {
org := seedOrg(t)
userID, ctx := seedRecruiter(t, org, "Departing Recruiter")
svc := reqSvc()
cand := seedPipelineCandidate(t, org, "Some Candidate")
if _, err := svc.LogActivity(ctx, recruiting.LogActivityInput{
Kind: "call", Outcome: "connected", CandidateID: cand.String(), Body: "Spoke at length"}); err != nil {
t.Fatalf("log: %v", err)
}
if _, err := migrator.Exec(ctxOrg(org), `DELETE FROM users WHERE id = $1`, userID); err != nil {
t.Fatalf("delete the recruiter: %v", err)
}
list, err := svc.ListActivities(ctx, recruiting.ActivityFilter{}, 100, 0)
if err != nil {
t.Fatalf("list after deletion: %v", err)
}
if list.Total != 1 {
t.Fatalf("%d activities survived the recruiter's deletion, want 1", list.Total)
}
a := list.Activities[0]
if a.ActorUserID != "" {
t.Errorf("actor_user_id = %q, want it nulled by the referential action", a.ActorUserID)
}
if a.ActorName != "Departing Recruiter" {
t.Errorf("actor name = %q, want the name recorded at write time", a.ActorName)
}
// And the summary still attributes the work rather than losing it.
s, err := svc.ActivitySummary(ctx, "")
if err != nil {
t.Fatalf("summary: %v", err)
}
if s.TotalLogged != 1 {
t.Errorf("summary total = %d after the recruiter left, want 1", s.TotalLogged)
}
}
// TestActivityTenantIsolation, with a control.
func TestActivityTenantIsolation(t *testing.T) {
svcA, ctxA, _, candsA := newPipeline(t, 1, 1)
if _, err := svcA.LogActivity(ctxA, recruiting.LogActivityInput{
Kind: "call", CandidateID: candsA[0].String(), Body: "Tenant A call"}); err != nil {
t.Fatalf("log in A: %v", err)
}
_, ctxB, _, _ := newPipeline(t, 1, 1)
listB, err := svcA.ListActivities(ctxB, recruiting.ActivityFilter{IncludeCaptured: true}, 100, 0)
if err != nil {
t.Fatalf("list in B: %v", err)
}
for _, x := range listB.Activities {
if x.Body == "Tenant A call" {
t.Fatal("tenant B can see tenant A's activity")
}
}
sB, err := svcA.ActivitySummary(ctxB, "")
if err != nil {
t.Fatalf("summary in B: %v", err)
}
for _, r := range sB.Recruiters {
if r.Name == "Pipeline Recruiter" && r.Logged > 0 {
// Both tenants seed a recruiter with the same name, so the check is on the count being
// confined to B's own rows rather than on the name being unique.
if sB.TotalLogged > 0 {
t.Fatalf("tenant B's summary counts %d activities it did not create", sB.TotalLogged)
}
}
}
// Control: A still sees its own.
listA, err := svcA.ListActivities(ctxA, recruiting.ActivityFilter{}, 100, 0)
if err != nil || listA.Total != 1 {
t.Fatalf("tenant A sees %d of its own activities, want 1 (err=%v)", listA.Total, err)
}
}