Skip to content
quarry Artifact [27d634ca49]
Login

Artifact [27d634ca49]

Artifact 27d634ca49af97f640b7aceea3b417b0bb342bf47d851cf47e906e2459977888:


package evt

import "testing"

// A subscription naming a noun selects every action on it, including ones
// added after the subscription was written. That is the entire argument for
// noun-and-action over Gitea's one-name-per-verb, so it is the thing to pin.
func TestANounSelectsEveryActionOnIt(t *testing.T) {
	if !TicketClosed.Matches("ticket") {
		t.Error(`"ticket" does not select ticket.closed`)
	}
	if !TicketOpened.Matches("ticket") {
		t.Error(`"ticket" does not select ticket.opened`)
	}
	if !TicketClosed.Matches("ticket.closed") {
		t.Error("an exact kind does not select itself")
	}
	// Precision is available where it is wanted.
	if TicketOpened.Matches("ticket.closed") {
		t.Error("ticket.closed selected ticket.opened")
	}
	// A noun does not reach across nouns, however similar the names.
	if LandingOpened.Matches("ticket") {
		t.Error("a noun selected another noun's event")
	}
	// A pattern naming a noun that does not exist selects nothing rather
	// than everything — the failure of a typo must be silence, not a
	// firehose.
	if TicketClosed.Matches("tickets") || TicketClosed.Matches("") {
		t.Error("a bad pattern matched")
	}
}

// The bus is a one-hour window, so what happens to a missed event depends on
// whether it can be re-derived from the store. Getting this classification
// wrong is how an operational fact — a failed mirror pull — silently does not
// happen, so each noun is asserted rather than the rule restated.
func TestDerivedEventsAreExactlyTheOnesTheStoreHolds(t *testing.T) {
	for _, k := range []Kind{CheckinCreated, BranchMoved, TagCreated,
		TicketClosed, WikiEdited, TechnoteCreated, ForumPosted, LandingAccepted} {
		if !k.Derived() {
			t.Errorf("%s is a reading of stored artifacts and should be derived", k)
		}
	}
	// None of these is in the DAG: missing one loses the fact, so each
	// must be recorded in OLTP as it is raised.
	for _, k := range []Kind{CICompleted, MirrorFailed, RepoCreated,
		AccountCaps, CloneCloned} {
		if k.Derived() {
			t.Errorf("%s is not in the store and must not be treated as re-derivable", k)
		}
	}
}

// An unknown name is refused where the subscription is written. A pipeline
// that never runs and a pipeline that runs and fails look nothing alike to
// the person waiting for it.
func TestUnknownKindsAreRefused(t *testing.T) {
	for _, bad := range []Kind{"ticket.exploded", "issue.opened", "checkin", ""} {
		if bad.Valid() {
			t.Errorf("%q was accepted into the vocabulary", bad)
		}
	}
	for _, k := range All {
		if !k.Valid() {
			t.Errorf("%s is in All but not Valid", k)
		}
		if k.Noun() == "" || k.Action() == "" {
			t.Errorf("%s is not noun.action", k)
		}
	}
}

// The vocabulary is a published interface: a name in someone else's pipeline
// definition. This pins the set so a rename is a deliberate act with a
// failing test attached, not a refactor that quietly breaks every store.
func TestTheVocabularyIsFixed(t *testing.T) {
	// 46 since RFC 0020's unversioned files gained a noun, uv.changed,
	// after RFC 0018's teams gained theirs. The RFC 0005 table carries
	// the rows.
	if got, want := len(All), 46; got != want {
		t.Errorf("the vocabulary changed size: %d, was %d — if deliberate, say so in RFC 0005", got, want)
	}
	want := []string{"checkin", "branch", "tag", "ticket", "wiki", "technote",
		"forum", "attachment", "landing", "ci", "mirror", "uv", "repo", "account", "team",
		"stamp", "clone", "error"}
	got := Nouns()
	if len(got) != len(want) {
		t.Fatalf("nouns = %v, want %v", got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("nouns = %v, want %v", got, want)
		}
	}
}

// The subject is the subscription interface: dagnats triggers take a NATS
// subject filter, so the vocabulary has to survive being taken apart by
// wildcards. A noun or action containing a dot would silently break every
// filter that assumed the shape.
func TestEverySubjectHasExactlyFiveTokens(t *testing.T) {
	for _, k := range All {
		got := k.Subject("quarry")
		if n := len(splitDots(got)); n != 5 {
			t.Errorf("%s -> %s has %d tokens, want 5 (quarry.evt.repo.noun.action)", k, got, n)
		}
	}
	if got, want := LandingOpened.Subject("quarry"), "quarry.evt.quarry.landing.opened"; got != want {
		t.Errorf("subject = %q, want %q", got, want)
	}
}

func splitDots(s string) []string {
	var out []string
	start := 0
	for i := 0; i < len(s); i++ {
		if s[i] == '.' {
			out = append(out, s[start:i])
			start = i + 1
		}
	}
	return append(out, s[start:])
}

// The vocabulary must stay off zit's DAG-transport root. The ingest
// pipeline's fold consumer subscribes to `zit.evt.<repo>.>` and parses
// everything it receives as an artifact, so an event published under that
// root is not merely sharing a stream — it is handed to a consumer that
// will try to fold it. This test is the tripwire on moving it back.
func TestVocabularyStaysOffTheIngestRoot(t *testing.T) {
	for _, k := range All {
		if s := k.Subject("quarry"); len(s) < 7 || s[:7] != "quarry." {
			t.Fatalf("%s publishes to %q; the vocabulary root must be quarry.", k, s)
		}
	}
}

// RFC 0016 §Events puts attachments in the vocabulary, so a webhook or a
// digest can say a file arrived. They are read off the A card the store
// already holds, which makes them derived.
func TestAttachmentKindsAreInTheVocabulary(t *testing.T) {
	for _, k := range []Kind{AttachmentAdded, AttachmentRemoved} {
		if !k.Valid() {
			t.Errorf("FAIL: %s is not in All", k)
		}
		if !k.Derived() {
			t.Errorf("FAIL: %s is an A-card reading and must be derivable", k)
		}
		if !k.Matches("attachment") {
			t.Errorf("FAIL: the noun does not select %s", k)
		}
	}
	if AttachmentAdded.Matches("attachment.removed") {
		t.Error("FAIL: removed selected added")
	}
	t.Log("ok: attachment.added and attachment.removed are nameable")
}

// An unversioned file written or removed (RFC 0020). Operational: the
// files live in a bucket, not in artifacts an index walk could re-read,
// so the catalog row is the record and the event its notice.
func TestUVChangedIsInTheVocabulary(t *testing.T) {
	if !UVChanged.Valid() || UVChanged.Derived() || !UVChanged.Matches("uv") || UVChanged.Subject("quarry") != "quarry.evt.quarry.uv.changed" {
		t.Fatalf("FAIL: uv.changed valid=%v derived=%v", UVChanged.Valid(), UVChanged.Derived())
	}
	t.Log("ok: uv.changed is nameable and operational")
}