Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/integration/task_integration_test.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/integration/task_integration_test.go |
| Original SHA-256 | 55e3d937e88aa4c44b57686aa2e06747f5ccd34f8b6e6559d1709c8e641c21ce |
| Git blob SHA | edc60f32560d42c61d4f30560e4b72ba465ee674 |
| Reading-copy SHA-256 | 55e3d937e88aa4c44b57686aa2e06747f5ccd34f8b6e6559d1709c8e641c21ce |
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/task_integration_test.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/integration/task_integration_test.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓290 lines · 9.5 KB
//go:build integration
package integration
import (
"testing"
"time"
"github.com/google/uuid"
"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.
func strPtr(s string) *string { return &s }
// TestTaskCanBeClosedReopenedAndCancelled. Reopening was impossible before 0057 because completed_at was
// never cleared and the CHECK refused the row.
func TestTaskCanBeClosedReopenedAndCancelled(t *testing.T) {
org := seedOrg(t)
_, ctx := seedRecruiter(t, org, "Queue Owner")
svc := reqSvc()
task, err := svc.CreateTask(ctx, recruiting.CreateTaskInput{
Title: "Call the hiring manager", DueAt: time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339)})
if err != nil {
t.Fatalf("create: %v", err)
}
if task.AssignedTo == "" {
t.Error("a task created with no assignee was not defaulted to its creator")
}
id := uuid.MustParse(task.ID)
done, err := svc.UpdateTask(ctx, id, recruiting.UpdateTaskInput{
ExpectedUpdatedAt: task.UpdatedAt, Status: strPtr("done")})
if err != nil {
t.Fatalf("close: %v", err)
}
if done.CompletedAt == "" {
t.Error("closing a task did not stamp completed_at")
}
if done.Bucket != "none" {
t.Errorf("a closed task is in bucket %q, want none: a completed task must never read as overdue", done.Bucket)
}
reopened, err := svc.UpdateTask(ctx, id, recruiting.UpdateTaskInput{
ExpectedUpdatedAt: done.UpdatedAt, Status: strPtr("open")})
if err != nil {
t.Fatalf("reopen: %v", err)
}
if reopened.CompletedAt != "" {
t.Error("reopening left a completion time, so the task still reads as finished")
}
cancelled, err := svc.UpdateTask(ctx, id, recruiting.UpdateTaskInput{
ExpectedUpdatedAt: reopened.UpdatedAt, Status: strPtr("cancelled")})
if err != nil {
t.Fatalf("cancel: %v", err)
}
if cancelled.CompletedAt != "" {
t.Error("a cancelled task carries a completion time")
}
}
// TestQueueBucketsUseTheOrganizationDay. This is the defect with the widest blast radius: an evening
// task in a US timezone is the next UTC day, so a UTC bucket would call today's work tomorrow's.
func TestQueueBucketsUseTheOrganizationDay(t *testing.T) {
org := seedOrg(t)
// The timezone is an owner setting, so this must act as one. Seeding a recruiter here is what the
// service now correctly refuses.
_, ctx := seedRoleUser(t, org, "TZ Owner", "owner")
svc := reqSvc()
if _, err := svc.SetTimeZone(ctx, "America/Chicago"); err != nil {
t.Fatalf("set time zone: %v", err)
}
// 23:30 Chicago today is already tomorrow in UTC. Bucketed on the organization's day it is today.
loc, err := time.LoadLocation("America/Chicago")
if err != nil {
t.Skipf("tz database unavailable: %v", err)
}
nowLocal := time.Now().In(loc)
lateToday := time.Date(nowLocal.Year(), nowLocal.Month(), nowLocal.Day(), 23, 30, 0, 0, loc)
if lateToday.Before(time.Now()) {
// Already past 23:30 locally, so use tomorrow and assert the soon bucket instead.
lateToday = lateToday.Add(24 * time.Hour)
}
task, err := svc.CreateTask(ctx, recruiting.CreateTaskInput{
Title: "Late in the day", DueAt: lateToday.Format(time.RFC3339)})
if err != nil {
t.Fatalf("create: %v", err)
}
wantBucket := "today"
if lateToday.In(loc).Day() != nowLocal.Day() {
wantBucket = "soon"
}
if task.Bucket != wantBucket {
t.Errorf("a task due at %s bucketed as %q, want %q in America/Chicago",
lateToday.Format(time.RFC3339), task.Bucket, wantBucket)
}
q, err := svc.TaskQueueView(ctx, "", false)
if err != nil {
t.Fatalf("queue: %v", err)
}
if q.TimeZone != "America/Chicago" {
t.Errorf("queue reports timezone %q, want America/Chicago", q.TimeZone)
}
// An invalid zone must be refused rather than silently stored and then failing every later query.
if _, err := svc.SetTimeZone(ctx, "Mars/Olympus"); err == nil {
t.Error("an invalid IANA time zone was accepted")
}
}
// TestQueueCountsAgreeWithTheRowsBelowThem. A header that contradicts its own list is worse than no
// header, which is why the counts are derived from the returned rows rather than a second query.
func TestQueueCountsAgreeWithTheRows(t *testing.T) {
org := seedOrg(t)
_, ctx := seedRecruiter(t, org, "Counter")
svc := reqSvc()
now := time.Now().UTC()
for _, d := range []time.Duration{-72 * time.Hour, -1 * time.Hour, 30 * time.Hour, 96 * time.Hour, 800 * time.Hour} {
if _, err := svc.CreateTask(ctx, recruiting.CreateTaskInput{
Title: "Task " + d.String(), DueAt: now.Add(d).Format(time.RFC3339)}); err != nil {
t.Fatalf("create: %v", err)
}
}
if _, err := svc.CreateTask(ctx, recruiting.CreateTaskInput{Title: "No due date"}); err != nil {
t.Fatalf("create undated: %v", err)
}
q, err := svc.TaskQueueView(ctx, "", false)
if err != nil {
t.Fatalf("queue: %v", err)
}
counted := map[string]int{}
for _, task := range q.Tasks {
counted[task.Bucket]++
}
if q.Overdue != counted["overdue"] {
t.Errorf("header says %d overdue, the list holds %d", q.Overdue, counted["overdue"])
}
if q.Today != counted["today"] {
t.Errorf("header says %d today, the list holds %d", q.Today, counted["today"])
}
if q.Soon != counted["soon"] {
t.Errorf("header says %d soon, the list holds %d", q.Soon, counted["soon"])
}
if q.Later != counted["later"] {
t.Errorf("header says %d later, the list holds %d", q.Later, counted["later"])
}
if q.NoDueDate != counted["none"] {
t.Errorf("header says %d undated, the list holds %d", q.NoDueDate, counted["none"])
}
if len(q.Tasks) != 6 {
t.Errorf("%d tasks in the queue, want 6", len(q.Tasks))
}
// Overdue must sort first, which is the whole point of the queue.
if len(q.Tasks) > 0 && q.Tasks[0].Bucket != "overdue" {
t.Errorf("the first task is %q, want the oldest overdue one first", q.Tasks[0].Bucket)
}
}
// TestTasksSurviveTheirAssignee. A task is work that still needs doing, so losing it when someone leaves
// loses the work rather than the record of it.
func TestTasksSurviveTheirAssignee(t *testing.T) {
org := seedOrg(t)
userID, ctx := seedRecruiter(t, org, "Departing Owner")
svc := reqSvc()
if _, err := svc.CreateTask(ctx, recruiting.CreateTaskInput{
Title: "Chase the signed contract", AssignedTo: userID.String()}); err != nil {
t.Fatalf("create: %v", err)
}
if _, err := migrator.Exec(ctxOrg(org), `DELETE FROM users WHERE id = $1`, userID); err != nil {
t.Fatalf("delete the user: %v", err)
}
// The context's user is gone, so query as another member of the same tenant.
_, ctx2 := seedRecruiter(t, org, "Successor")
q, err := svc.TaskQueueView(ctx2, "unassigned", false)
if err != nil {
t.Fatalf("queue: %v", err)
}
if len(q.Tasks) != 1 {
t.Fatalf("%d unassigned tasks after the owner left, want 1", len(q.Tasks))
}
if q.Tasks[0].AssignedTo != "" {
t.Error("the surviving task still names a deleted user")
}
if q.Unassigned != 1 {
t.Errorf("unassigned count = %d, want 1", q.Unassigned)
}
if q.Tasks[0].Title != "Chase the signed contract" {
t.Errorf("the surviving task is %q", q.Tasks[0].Title)
}
}
// TestTaskOrderingIsDeterministic. Without a total order, two tasks sharing a due date can swap between
// pages so one is shown twice and another never.
func TestTaskOrderingIsDeterministic(t *testing.T) {
org := seedOrg(t)
_, ctx := seedRecruiter(t, org, "Pager")
svc := reqSvc()
same := time.Now().UTC().Add(48 * time.Hour).Format(time.RFC3339)
for i := 0; i < 12; i++ {
if _, err := svc.CreateTask(ctx, recruiting.CreateTaskInput{
Title: "Same due date", DueAt: same}); err != nil {
t.Fatalf("create: %v", err)
}
}
var first []string
for run := 0; run < 4; run++ {
list, err := svc.ListTasks(ctx, "", "", "", 100, 0)
if err != nil {
t.Fatalf("list: %v", err)
}
ids := make([]string, len(list.Tasks))
for i, x := range list.Tasks {
ids[i] = x.ID
}
if run == 0 {
first = ids
continue
}
for i := range ids {
if ids[i] != first[i] {
t.Fatalf("ordering changed between identical queries at position %d", i)
}
}
}
// And paging covers every row exactly once.
seen := map[string]bool{}
for offset := 0; offset < 12; offset += 5 {
page, err := svc.ListTasks(ctx, "", "", "", 5, offset)
if err != nil {
t.Fatalf("page at %d: %v", offset, err)
}
for _, x := range page.Tasks {
if seen[x.ID] {
t.Fatalf("task %s appeared on two pages", x.ID)
}
seen[x.ID] = true
}
}
if len(seen) != 12 {
t.Errorf("paging covered %d of 12 tasks", len(seen))
}
}
// TestTaskTenantIsolation, with a control.
func TestTaskTenantIsolation(t *testing.T) {
orgA := seedOrg(t)
_, ctxA := seedRoleUser(t, orgA, "Tenant A", "owner")
svc := reqSvc()
if _, err := svc.CreateTask(ctxA, recruiting.CreateTaskInput{Title: "Tenant A private task"}); err != nil {
t.Fatalf("create in A: %v", err)
}
orgB := seedOrg(t)
_, ctxB := seedRoleUser(t, orgB, "Tenant B", "owner")
qB, err := svc.TaskQueueView(ctxB, "", false)
if err != nil {
t.Fatalf("queue in B: %v", err)
}
for _, x := range qB.Tasks {
if x.Title == "Tenant A private task" {
t.Fatal("tenant B can see tenant A's task")
}
}
// Each tenant carries its own working day.
if _, err := svc.SetTimeZone(ctxA, "America/Los_Angeles"); err != nil {
t.Fatalf("set tz in A: %v", err)
}
qA, _ := svc.TaskQueueView(ctxA, "", false)
qB2, _ := svc.TaskQueueView(ctxB, "", false)
if qA.TimeZone != "America/Los_Angeles" {
t.Errorf("tenant A timezone = %q", qA.TimeZone)
}
if qB2.TimeZone == "America/Los_Angeles" {
t.Error("changing tenant A's timezone changed tenant B's working day")
}
// Control: A still sees its own.
if len(qA.Tasks) != 1 {
t.Errorf("tenant A sees %d of its own tasks, want 1", len(qA.Tasks))
}
}