Skip to content

Commit ea617e7

Browse files
committed
test(golang): code generated with -for and from a policy runs against the guest
`stashgen` writes a file three ways: from a struct's own tags, from tags declared for a type in another package (`-for`), and from a policy (`stashgen.Generate`). Only the tag path's output was ever executed, in the round-trip tests over the deterministic guest. The other two were compiled against the stub SDK and compared as text, so a file that built and read correctly but sealed under another label, or lost a term, would have passed. `testmember.Member` is a struct with User's fields that carries no tags. `testusers` declares them for it with `-for` (`memberStash`, `EncryptMember`), and `genmember` writes `testpolicy` from a policy with the same decisions, through `go generate` like every generated file, so `go generate ./... && git diff --exit-code` covers both. The new test seals the same values through all three, holds the -for and policy terms to the tag path's bytes, opens each path's rows through the cipher and the client and the tag path's rows through the other two, and derives probes through each path's `Fields`.
1 parent 1b4fa6a commit ea617e7

7 files changed

Lines changed: 624 additions & 1 deletion

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package encrypt_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"testing"
7+
8+
"github.com/cipherstash/stack/languages/golang/encrypt"
9+
"github.com/cipherstash/stack/languages/golang/encrypt/internal/testmember"
10+
"github.com/cipherstash/stack/languages/golang/encrypt/internal/testpolicy"
11+
"github.com/cipherstash/stack/languages/golang/encrypt/internal/testusers"
12+
)
13+
14+
// The generator writes a file three ways: from a struct's own tags, from
15+
// tags declared for a type in another package (-for), and from a policy
16+
// (stashgen.Generate). The golden tests only compile the second and third;
17+
// this runs them against the deterministic guest, beside the tag path, for
18+
// one declaration (testusers.User's) over one set of values, and holds the
19+
// three to the same bytes.
20+
21+
var members = []testmember.Member{
22+
{ID: 1, Age: 34, Email: "alice@example.com", Notes: "likes cats"},
23+
{ID: 2, Age: 29, Email: "bob@example.com", Notes: "likes dogs"},
24+
}
25+
26+
func asUsers(ms []testmember.Member) []testusers.User {
27+
out := make([]testusers.User, len(ms))
28+
for i, m := range ms {
29+
out[i] = testusers.User{ID: m.ID, Age: m.Age, Email: m.Email, Notes: m.Notes}
30+
}
31+
return out
32+
}
33+
34+
func TestGeneratedCodeFromForAndFromAPolicyRunsAgainstTheGuest(t *testing.T) {
35+
c := deterministicClient(t)
36+
ctx := context.Background()
37+
cipher := c.DefaultKeyset()
38+
39+
tagged, err := testusers.Encrypt(ctx, cipher, asUsers(members))
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
forPath, err := testusers.EncryptMember(ctx, cipher, members)
44+
if err != nil {
45+
t.Fatalf("the -for path: %v", err)
46+
}
47+
policyPath, err := testpolicy.Encrypt(ctx, cipher, members)
48+
if err != nil {
49+
t.Fatalf("the policy path: %v", err)
50+
}
51+
52+
// One declaration, three authors, the same terms.
53+
for i := range members {
54+
for name, got := range map[string]struct{ ageEq, ageOre, emailEq, emailMatch []byte }{
55+
"the -for path": {forPath[i].Age.Equality, forPath[i].Age.Ore, forPath[i].Email.Equality, forPath[i].Email.Match},
56+
"the policy path": {policyPath[i].Age.Equality, policyPath[i].Age.Ore, policyPath[i].Email.Equality, policyPath[i].Email.Match},
57+
} {
58+
want := tagged[i]
59+
if !bytes.Equal(got.ageEq, want.Age.Equality) || !bytes.Equal(got.ageOre, want.Age.Ore) || !bytes.Equal(got.emailEq, want.Email.Equality) || !bytes.Equal(got.emailMatch, want.Email.Match) {
60+
t.Errorf("row %d: %s derives other terms than the tag path", i, name)
61+
}
62+
}
63+
if len(forPath[i].Notes.Ciphertext) == 0 || len(policyPath[i].Notes.Ciphertext) == 0 || forPath[i].ID != members[i].ID || policyPath[i].ID != members[i].ID {
64+
t.Errorf("row %d: outputs missing: %+v %+v", i, forPath[i], policyPath[i])
65+
}
66+
}
67+
68+
// Each path opens its own rows, through the cipher and the client.
69+
for _, d := range []struct {
70+
name string
71+
by encrypt.Decrypter
72+
}{{"cipher", cipher}, {"client", c}} {
73+
back, err := testusers.DecryptMember(ctx, d.by, forPath)
74+
if err != nil {
75+
t.Fatalf("DecryptMember through the %s: %v", d.name, err)
76+
}
77+
for i := range members {
78+
if back[i] != members[i] {
79+
t.Fatalf("the -for path through the %s: row %d = %+v, want %+v", d.name, i, back[i], members[i])
80+
}
81+
}
82+
back, err = testpolicy.Decrypt(ctx, d.by, policyPath)
83+
if err != nil {
84+
t.Fatalf("testpolicy.Decrypt through the %s: %v", d.name, err)
85+
}
86+
for i := range members {
87+
if back[i] != members[i] {
88+
t.Fatalf("the policy path through the %s: row %d = %+v, want %+v", d.name, i, back[i], members[i])
89+
}
90+
}
91+
}
92+
93+
// And each opens the tag path's rows: the stored shape is one shape.
94+
crossed := make([]testusers.EncryptedMember, len(tagged))
95+
for i, e := range tagged {
96+
crossed[i] = testusers.EncryptedMember{
97+
ID: e.ID,
98+
Age: testusers.EncryptedMemberAge(e.Age),
99+
Email: testusers.EncryptedMemberEmail(e.Email),
100+
Notes: testusers.EncryptedMemberNotes(e.Notes),
101+
}
102+
}
103+
if back, err := testusers.DecryptMember(ctx, cipher, crossed); err != nil || back[0] != members[0] {
104+
t.Fatalf("the -for path opening the tag path's rows: %v %+v", err, back)
105+
}
106+
viaPolicy := make([]testpolicy.EncryptedMember, len(tagged))
107+
for i, e := range tagged {
108+
viaPolicy[i] = testpolicy.EncryptedMember{
109+
ID: e.ID,
110+
Age: testpolicy.EncryptedMemberAge(e.Age),
111+
Email: testpolicy.EncryptedMemberEmail(e.Email),
112+
Notes: testpolicy.EncryptedMemberNotes(e.Notes),
113+
}
114+
}
115+
if back, err := testpolicy.Decrypt(ctx, cipher, viaPolicy); err != nil || back[1] != members[1] {
116+
t.Fatalf("the policy path opening the tag path's rows: %v %+v", err, back)
117+
}
118+
119+
// A term derived through each path's Fields is the tag path's.
120+
probe, err := testusers.Fields.Email.Equality(ctx, cipher, "bob@example.com")
121+
if err != nil {
122+
t.Fatal(err)
123+
}
124+
forProbe, err := testusers.MemberFields.Email.Equality(ctx, cipher, "bob@example.com")
125+
if err != nil || !forProbe.Equal(probe) {
126+
t.Fatalf("the -for path's probe: %v, equal=%v", err, forProbe.Equal(probe))
127+
}
128+
policyProbe, err := testpolicy.Fields.Email.Equality(ctx, cipher, "bob@example.com")
129+
if err != nil || !policyProbe.Equal(probe) {
130+
t.Fatalf("the policy path's probe: %v, equal=%v", err, policyProbe.Equal(probe))
131+
}
132+
ore, err := testpolicy.Fields.Age.Ore(ctx, cipher, 29)
133+
if err != nil || !bytes.Equal(ore, tagged[1].Age.Ore) {
134+
t.Fatalf("the policy path's ORE probe: %v", err)
135+
}
136+
if !probe.Equal(tagged[1].Email.Equality) || probe.Equal(tagged[0].Email.Equality) {
137+
t.Error("the probe does not single out bob's row")
138+
}
139+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Command genmember writes testpolicy's generated file from a policy: the
2+
// stashgen.Generate path, over testmember.Member, with the declaration
3+
// testusers.User carries as tags. go generate runs it from the testpolicy
4+
// directory, so the file lands there and CI's generate-and-diff check
5+
// covers it like every other generated file.
6+
package main
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"os"
12+
13+
"github.com/cipherstash/stack/languages/golang/encrypt/internal/testmember"
14+
"github.com/cipherstash/stack/languages/golang/encrypt/policy"
15+
"github.com/cipherstash/stack/languages/golang/stashgen"
16+
)
17+
18+
// facts is what a schema source would say about Member: a fact per field,
19+
// with no annotations, so the rules decide by field name.
20+
func facts(any) ([]policy.Fact, error) {
21+
f := func(name, goName, kind string) policy.Fact {
22+
return policy.Fact{Message: "testmember.Member", Name: name, GoName: goName, Kind: kind}
23+
}
24+
return []policy.Fact{
25+
f("id", "ID", "int64"),
26+
f("age", "Age", "uint32"),
27+
f("email", "Email", "string"),
28+
f("notes", "Notes", "string"),
29+
}, nil
30+
}
31+
32+
// rules is testusers.User's declaration as a policy.
33+
var rules = policy.ForMessage(&testmember.Member{}, policy.Context("users"), policy.FirstOf(
34+
policy.When(policy.Field("id"), policy.Passthrough()),
35+
policy.When(policy.Field("age"), policy.EncryptIndex(policy.Equality, policy.Ore)),
36+
policy.When(policy.Field("email"), policy.EncryptIndex(policy.Equality, policy.Match())),
37+
policy.When(policy.Field("notes"), policy.Encrypt()),
38+
))
39+
40+
func main() {
41+
if err := stashgen.Generate(context.Background(), policy.SourceFunc(facts), rules, "member_stash.go"); err != nil {
42+
fmt.Fprintln(os.Stderr, err)
43+
os.Exit(1)
44+
}
45+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Package testmember holds a struct the encrypt tests encrypt through the
2+
// two generator paths that take a type the program does not own: the
3+
// `-for` flag (testusers declares the tags for Member in a struct of its
4+
// own) and stashgen.Generate from a policy (genmember writes testpolicy).
5+
// Member has the fields, kinds and indexes of testusers.User, so the three
6+
// paths seal the same declaration and the round-trip tests compare their
7+
// bytes.
8+
package testmember
9+
10+
// Member cannot carry stash tags here: the point is that the tags live
11+
// elsewhere. Its field names are User's so the declarations line up.
12+
type Member struct {
13+
ID int64
14+
Age uint32
15+
Email string
16+
Notes string
17+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Package testpolicy holds the file stashgen.Generate writes for
2+
// testmember.Member from a policy (see internal/genmember): the generated
3+
// Encrypt, Decrypt and Fields over *testmember.Member, with the declaration
4+
// testusers.User carries as tags. The encrypt tests run it against the
5+
// deterministic guest beside the tag path and the -for path, so code from
6+
// a policy is executed, not only compiled. `go generate ./...` rewrites
7+
// member_stash.go, and CI fails when that changes it.
8+
package testpolicy
9+
10+
//go:generate go run github.com/cipherstash/stack/languages/golang/encrypt/internal/genmember

0 commit comments

Comments
 (0)