| 1 | package hostrpc |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/json" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | type contractSmall struct{} |
| 11 | |
| 12 | func (*contractSmall) One() string { return "" } |
| 13 | |
| 14 | type contractLarge struct{} |
| 15 | |
| 16 | func (*contractLarge) One() string { return "" } |
| 17 | func (*contractLarge) Two() int { return 0 } |
| 18 | |
| 19 | func TestContractDigestIsStableAndSensitiveToCommands(t *testing.T) { |
| 20 | small := Build(mustRegistry(t, &contractSmall{}, nil), []string{"b:event", "a:event", "b:event"}) |
| 21 | again := Build(mustRegistry(t, &contractSmall{}, nil), []string{"a:event", "b:event"}) |
| 22 | large := Build(mustRegistry(t, &contractLarge{}, nil), []string{"a:event", "b:event"}) |
| 23 | fewer := Build(mustRegistry(t, &contractSmall{}, nil), []string{"a:event"}) |
| 24 | |
| 25 | if small.Digest() != again.Digest() { |
| 26 | t.Fatalf("digest changed across identical builds: %s vs %s", small.Digest(), again.Digest()) |
| 27 | } |
| 28 | if small.Digest() == large.Digest() { |
| 29 | t.Fatal("adding a command must change the digest") |
| 30 | } |
| 31 | if small.Digest() == fewer.Digest() { |
| 32 | t.Fatal("removing an event must change the digest") |
| 33 | } |
| 34 | if !strings.HasPrefix(small.Digest(), "sha256:") || len(small.Digest()) != len("sha256:")+64 { |
| 35 | t.Fatalf("digest format = %q", small.Digest()) |
| 36 | } |
| 37 | if small.ProtocolVersion != ProtocolVersion { |
| 38 | t.Fatalf("protocol version = %d", small.ProtocolVersion) |
| 39 | } |
| 40 | if strings.Join(small.Events, ",") != "a:event,b:event" { |
| 41 | t.Fatalf("events = %v", small.Events) |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | func TestContractCanonicalJSONHasSortedKeysAndNoWhitespace(t *testing.T) { |
| 46 | c := Build(mustRegistry(t, &fixtureTarget{}, nil), []string{"z", "a"}) |
| 47 | canonical := c.Canonical() |
| 48 | if bytes.ContainsAny(canonical, "\n\t") || bytes.Contains(canonical, []byte(`": `)) { |
| 49 | t.Fatal("canonical JSON must not contain layout whitespace") |
| 50 | } |
| 51 | if !json.Valid(canonical) { |
| 52 | t.Fatal("canonical JSON is invalid") |
| 53 | } |
| 54 | if !bytes.HasPrefix(canonical, []byte(`{"commands":[{"cancellation":"before-dispatch","name":"Explode","params":[]`)) { |
| 55 | t.Fatalf("keys not sorted: %.80s", canonical) |
| 56 | } |
| 57 | var buf bytes.Buffer |
| 58 | if err := WriteJSON(&buf, c); err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | var indented, compact any |
| 62 | if err := json.Unmarshal(buf.Bytes(), &indented); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | if err := json.Unmarshal(canonical, &compact); err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | if !bytes.HasSuffix(buf.Bytes(), []byte("}\n")) { |
| 69 | t.Fatal("WriteJSON must end with a newline") |
| 70 | } |
| 71 | compactAgain, _ := json.Marshal(indented) |
| 72 | compactRef, _ := json.Marshal(compact) |
| 73 | if !bytes.Equal(compactAgain, compactRef) { |
| 74 | t.Fatal("WriteJSON content differs from Canonical") |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | func TestContractOmitsNilCollectionsAsEmpty(t *testing.T) { |
| 79 | type bare struct{} |
| 80 | c := Build(mustRegistry(t, &bare{}, nil), nil) |
| 81 | canonical := string(c.Canonical()) |
| 82 | for _, want := range []string{`"commands":[]`, `"events":[]`, `"types":{}`} { |
| 83 | if !strings.Contains(canonical, want) { |
| 84 | t.Errorf("canonical %s lacks %s", canonical, want) |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 |