| 1 | package seeds |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | func putOwners(t *testing.T, s release.Store, body, ifMatch string) string { |
| 14 | t.Helper() |
| 15 | etag, err := s.Put(context.Background(), LegacyOwnersKey, []byte(body), ifMatch) |
| 16 | if err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | return etag |
| 20 | } |
| 21 | |
| 22 | func TestLegacyOwners(t *testing.T) { |
| 23 | ctx := context.Background() |
| 24 | s := release.NewDirStore(t.TempDir()) |
| 25 | o, etag, err := ReadLegacyOwners(ctx, s) |
| 26 | if err != nil || etag != "" || o == nil || len(o.Names) != 0 { |
| 27 | t.Fatalf("absent = %+v %q %v", o, etag, err) |
| 28 | } |
| 29 | if _, ok := o.Lookup("shop"); ok { |
| 30 | t.Fatal("a name resolved from an absent file") |
| 31 | } |
| 32 | etag = putOwners(t, s, `{"v":1,"names":{"shop":{"account":"abc","app":"shop"}}}`, "") |
| 33 | o, got, err := ReadLegacyOwners(ctx, s) |
| 34 | if err != nil || got == "" || got != etag { |
| 35 | t.Fatalf("read = %q (put %q) %v", got, etag, err) |
| 36 | } |
| 37 | if got, ok := o.Lookup("shop"); !ok || got != (LegacyOwner{Account: "abc", App: "shop"}) { |
| 38 | t.Fatalf("lookup = %+v %v", got, ok) |
| 39 | } |
| 40 | if _, ok := o.Lookup("other"); ok { |
| 41 | t.Fatal("an unassigned name resolved") |
| 42 | } |
| 43 | if _, _, err := ReadLegacyOwners(ctx, s); err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | putOwners(t, s, `{garbage`, etag) |
| 47 | if _, _, err := ReadLegacyOwners(ctx, s); err == nil { |
| 48 | t.Fatal("garbage read as no assignments") |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | func TestLegacyOwnersEmptySet(t *testing.T) { |
| 53 | s := release.NewDirStore(t.TempDir()) |
| 54 | putOwners(t, s, `{"v":1}`, "") |
| 55 | o, etag, err := ReadLegacyOwners(context.Background(), s) |
| 56 | if err != nil || etag == "" || o == nil || o.Names == nil || len(o.Names) != 0 { |
| 57 | t.Fatalf("empty v1 file = %+v %q %v", o, etag, err) |
| 58 | } |
| 59 | if _, ok := o.Lookup("shop"); ok { |
| 60 | t.Fatal("a name resolved from an empty set") |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | // Every present-but-unusable file is an error and no owners, never an empty |
| 65 | // set: the swap treats "no assignment" as a refusal it can explain, and a |
| 66 | // read it cannot trust as a failure of its own. |
| 67 | func TestLegacyOwnersFailClosed(t *testing.T) { |
| 68 | good := `"names":{"shop":{"account":"abc","app":"shop"}}` |
| 69 | cases := map[string]string{ |
| 70 | "garbage": `{garbage`, |
| 71 | "empty body": ``, |
| 72 | "not an object": `[]`, |
| 73 | "null": `null`, |
| 74 | "version missing": `{` + good + `}`, |
| 75 | "version zero": `{"v":0,` + good + `}`, |
| 76 | "version unknown": `{"v":2,` + good + `}`, |
| 77 | "version negative": `{"v":-1,` + good + `}`, |
| 78 | "version a string": `{"v":"1",` + good + `}`, |
| 79 | "names a list": `{"v":1,"names":[]}`, |
| 80 | "entry a string": `{"v":1,"names":{"shop":"abc/shop"}}`, |
| 81 | "entry no account": `{"v":1,"names":{"shop":{"app":"shop"}}}`, |
| 82 | "entry no app": `{"v":1,"names":{"shop":{"account":"abc"}}}`, |
| 83 | "entry null": `{"v":1,"names":{"shop":null}}`, |
| 84 | "empty name": `{"v":1,"names":{"":{"account":"abc","app":"shop"}}}`, |
| 85 | "unknown field": `{"v":1,"extra":true,` + good + `}`, |
| 86 | "unknown entry key": `{"v":1,"names":{"shop":{"account":"abc","app":"shop","x":1}}}`, |
| 87 | "trailing object": `{"v":1,` + good + `}{"v":1}`, |
| 88 | "trailing garbage": `{"v":1,` + good + `}x`, |
| 89 | } |
| 90 | for name, body := range cases { |
| 91 | t.Run(name, func(t *testing.T) { |
| 92 | s := release.NewDirStore(t.TempDir()) |
| 93 | putOwners(t, s, body, "") |
| 94 | o, etag, err := ReadLegacyOwners(context.Background(), s) |
| 95 | if err == nil { |
| 96 | t.Fatalf("read %q = %+v, want an error", body, o) |
| 97 | } |
| 98 | if o != nil || etag != "" { |
| 99 | t.Fatalf("an error came with owners %+v / etag %q", o, etag) |
| 100 | } |
| 101 | if errors.Is(err, os.ErrNotExist) { |
| 102 | t.Fatalf("a bad file reads as absent: %v", err) |
| 103 | } |
| 104 | if !strings.Contains(err.Error(), "legacy route owners") { |
| 105 | t.Fatalf("error does not say what failed: %v", err) |
| 106 | } |
| 107 | }) |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | // A store error other than not-found is an error too. |
| 112 | func TestLegacyOwnersReadError(t *testing.T) { |
| 113 | boom := errors.New("bucket unreachable") |
| 114 | o, etag, err := ReadLegacyOwners(context.Background(), failingStore{Store: release.NewDirStore(t.TempDir()), err: boom}) |
| 115 | if !errors.Is(err, boom) || o != nil || etag != "" { |
| 116 | t.Fatalf("read error = %+v %q %v", o, etag, err) |
| 117 | } |
| 118 | var nilOwners *LegacyOwners |
| 119 | if _, ok := nilOwners.Lookup("shop"); ok { |
| 120 | t.Fatal("a nil set resolved a name") |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | type failingStore struct { |
| 125 | release.Store |
| 126 | err error |
| 127 | } |
| 128 | |
| 129 | func (f failingStore) Get(context.Context, string) ([]byte, string, error) { |
| 130 | return nil, "", f.err |
| 131 | } |
| 132 | |