| 1 | package moves |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/release" |
| 9 | ) |
| 10 | |
| 11 | const tLogPrefix = "moves/bab/app/a.go.example.com/mv_abcdefghijklmnopqrstuvwxyz/" |
| 12 | |
| 13 | func logEvent(identity, event string, at time.Time, actor string) LogEvent { |
| 14 | return LogEvent{V: 1, Op: "mv_abcdefghijklmnopqrstuvwxyz", Host: "a.go.example.com", Event: event, Identity: identity, Actor: actor, At: at} |
| 15 | } |
| 16 | |
| 17 | // Plan 6 Task 3 (preflight L10, L27): a log object's key is the transition's |
| 18 | // identity, never a counter. A key is written create-only: the first writer |
| 19 | // wins and creates it; a second writer of the same transition, or a replay, |
| 20 | // meets the existing object, which is success with nothing created. |
| 21 | func TestLogKeysAreTransitionIdentities(t *testing.T) { |
| 22 | ctx := context.Background() |
| 23 | s := release.NewDirStore(t.TempDir()) |
| 24 | t0 := time.Date(2026, 10, 11, 9, 0, 0, 0, time.UTC) |
| 25 | |
| 26 | // Two writers of landing-probing (two boxes, or a crash and a replay), |
| 27 | // with different bodies: one object, the first writer's. |
| 28 | created, err := WriteLog(ctx, s, tLogPrefix, logEvent("landing-probing", "probing", t0, "box-uk")) |
| 29 | if err != nil || !created { |
| 30 | t.Fatalf("first writer: created %v, %v", created, err) |
| 31 | } |
| 32 | created, err = WriteLog(ctx, s, tLogPrefix, logEvent("landing-probing", "probing", t0.Add(time.Second), "box-ie")) |
| 33 | if err != nil || created { |
| 34 | t.Fatalf("second writer: created %v, %v; want no object and no error", created, err) |
| 35 | } |
| 36 | // Three probe attempts: three objects. |
| 37 | for n, id := range []string{"probe_attempt-1", "probe_attempt-2", "probe_attempt-3"} { |
| 38 | if created, err := WriteLog(ctx, s, tLogPrefix, logEvent(id, "probe_attempt", t0.Add(time.Duration(n+2)*time.Second), "box-uk")); err != nil || !created { |
| 39 | t.Fatalf("%s: created %v, %v", id, created, err) |
| 40 | } |
| 41 | } |
| 42 | // A replayed cutting-stopped writes nothing new. |
| 43 | if created, err := WriteLog(ctx, s, tLogPrefix, logEvent("cutting-stopped", "stopped", t0.Add(-time.Minute), "box-ie")); err != nil || !created { |
| 44 | t.Fatalf("cutting-stopped: %v %v", created, err) |
| 45 | } |
| 46 | if created, err := WriteLog(ctx, s, tLogPrefix, logEvent("cutting-stopped", "stopped", t0.Add(-time.Minute), "box-ie")); err != nil || created { |
| 47 | t.Fatalf("replayed cutting-stopped: created %v, %v", created, err) |
| 48 | } |
| 49 | |
| 50 | keys, err := s.List(ctx, tLogPrefix+"log/") |
| 51 | if err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | if len(keys) != 5 { |
| 55 | t.Fatalf("keys %v, want five objects", keys) |
| 56 | } |
| 57 | for _, id := range []string{"landing-probing", "probe_attempt-1", "probe_attempt-2", "probe_attempt-3", "cutting-stopped"} { |
| 58 | if _, _, err := s.Get(ctx, LogKeyFor(tLogPrefix, id)); err != nil { |
| 59 | t.Fatalf("%s: %v", id, err) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | // ReadLog orders by At, never by key. |
| 64 | got, err := ReadLog(ctx, s, tLogPrefix) |
| 65 | if err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | var order []string |
| 69 | for _, ev := range got { |
| 70 | order = append(order, ev.Identity) |
| 71 | } |
| 72 | want := "cutting-stopped landing-probing probe_attempt-1 probe_attempt-2 probe_attempt-3" |
| 73 | if j := joinSpace(order); j != want { |
| 74 | t.Fatalf("ReadLog order %q, want %q", j, want) |
| 75 | } |
| 76 | if got[1].Actor != "box-uk" { |
| 77 | t.Fatalf("landing-probing is %+v, want the first writer's", got[1]) |
| 78 | } |
| 79 | |
| 80 | // An identity is one key segment. |
| 81 | for _, bad := range []string{"", "a/b", "..", "."} { |
| 82 | if _, err := WriteLog(ctx, s, tLogPrefix, logEvent(bad, "x", t0, "box")); err == nil { |
| 83 | t.Errorf("identity %q accepted", bad) |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | func joinSpace(xs []string) string { |
| 89 | out := "" |
| 90 | for i, x := range xs { |
| 91 | if i > 0 { |
| 92 | out += " " |
| 93 | } |
| 94 | out += x |
| 95 | } |
| 96 | return out |
| 97 | } |
| 98 | |