package hostops import ( "bytes" "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/geoip" "github.com/carlosframework/platform/internal/release" ) func geoipTestBlob(t *testing.T) (key string, body []byte) { t.Helper() body = bytes.Repeat([]byte("geoip-blob"), 100) sum := sha256.Sum256(body) return geoip.BlobKey(hex.EncodeToString(sum[:])), body } func geoipTestPointer(t *testing.T, key string, body []byte) []byte { t.Helper() sum := sha256.Sum256(body) sha := hex.EncodeToString(sum[:]) p := geoip.Pointer{V: 1, SHA256: sha, Key: geoip.BlobKey(sha), Size: int64(len(body)), DatabaseType: "DBIP-City-Lite", BuildEpoch: 1, Published: time.Unix(2, 0).UTC(), Origin: "manual", Edition: "test"} b, err := json.MarshalIndent(p, "", " ") if err != nil { t.Fatal(err) } return b } func TestClassifyGeoipIsExact(t *testing.T) { ctx := context.Background() key, body := geoipTestBlob(t) ptr := geoipTestPointer(t, key, body) feed := []byte(`{"v":1,"feed":"dbip-city-lite"}`) for _, c := range []struct { name string target MutationTarget family string }{ {"pointer put", MutationTarget{Surface: "bucket", Method: "Put", Key: geoip.PointerKey, After: ptr}, "geoip-pointer"}, {"pointer delete", MutationTarget{Surface: "bucket", Method: "Delete", Key: geoip.PointerKey, Before: ptr}, "geoip-pointer"}, {"pointer delete of an absent key", MutationTarget{Surface: "bucket", Method: "Delete", Key: geoip.PointerKey}, "geoip-pointer"}, {"feed put", MutationTarget{Surface: "bucket", Method: "Put", Key: geoip.FeedKey, After: feed}, "geoip-feed"}, {"feed delete", MutationTarget{Surface: "bucket", Method: "Delete", Key: geoip.FeedKey}, "geoip-feed"}, {"blob put (buffered)", MutationTarget{Surface: "bucket", Method: "Put", Key: key, After: body}, "geoip-blob"}, {"blob put (streamed, no body)", MutationTarget{Surface: "bucket", Method: "Put", Key: key}, "geoip-blob"}, {"blob delete", MutationTarget{Surface: "bucket", Method: "Delete", Key: key}, "geoip-blob"}, {"blob delete if match", MutationTarget{Surface: "bucket", Method: "DeleteIfMatch", Key: key, ETag: "x"}, "geoip-blob"}, } { got, err := ClassifyMutation(ctx, nil, nil, c.target) if err != nil { t.Errorf("%s: %v", c.name, err) continue } if got.Family != c.family || got.NonApp == "" || len(got.Scopes) != 0 || got.MailGate { t.Errorf("%s: classified %+v, want non-app family %s with no scopes", c.name, got, c.family) } } for _, c := range []struct { name string target MutationTarget }{ {"pointer put that is not a pointer", MutationTarget{Surface: "bucket", Method: "Put", Key: geoip.PointerKey, After: []byte(`{"v":1}`)}}, {"pointer put with no body", MutationTarget{Surface: "bucket", Method: "Put", Key: geoip.PointerKey}}, {"feed put that is not a status", MutationTarget{Surface: "bucket", Method: "Put", Key: geoip.FeedKey, After: []byte(`nope`)}}, {"blob put whose body is not its name", MutationTarget{Surface: "bucket", Method: "Put", Key: key, After: []byte("other")}}, {"blob with a short name", MutationTarget{Surface: "bucket", Method: "Put", Key: geoip.Prefix + "abc.mmdb"}}, {"blob with an upper-case name", MutationTarget{Surface: "bucket", Method: "Put", Key: geoip.Prefix + strings.ToUpper(strings.TrimSuffix(strings.TrimPrefix(key, geoip.Prefix), ".mmdb")) + ".mmdb"}}, {"nested key", MutationTarget{Surface: "bucket", Method: "Put", Key: geoip.Prefix + "x/current.json"}}, {"prefix itself", MutationTarget{Surface: "bucket", Method: "Delete", Key: geoip.Prefix}}, {"a stray file", MutationTarget{Surface: "bucket", Method: "Put", Key: geoip.Prefix + "notes.txt", After: []byte("x")}}, {"another store under a lookalike prefix", MutationTarget{Surface: "bucket", Method: "Put", Key: "control/geoip-other/current.json", After: ptr}}, } { if got, err := ClassifyMutation(context.Background(), nil, nil, c.target); !errors.Is(err, ErrUnclassified) { t.Errorf("%s: classified %+v, err %v; want ErrUnclassified", c.name, got, err) } } } // PutFrom must reach the raw store only through the classifier, only for an // exact blob key, and never buffer a blob. func TestMutationBoundaryPutFromIsClassified(t *testing.T) { ctx := context.Background() raw := release.NewDirStore(t.TempDir()) var seen []MutationTarget check := func(ctx context.Context, tg MutationTarget) error { seen = append(seen, tg) _, err := ClassifyMutation(ctx, raw, nil, tg) return err } b, err := NewMutationBoundary(true, raw, check) if err != nil { t.Fatal(err) } ps, ok := b.(release.PutStreamer) if !ok { t.Fatalf("%T is not a PutStreamer: PutObjectFrom would buffer a GeoIP blob through Put", b) } key, body := geoipTestBlob(t) if _, err := ps.PutFrom(ctx, key, bytes.NewReader(body), int64(len(body)), ""); err != nil { t.Fatalf("blob PutFrom: %v", err) } if len(seen) != 1 || seen[0].Key != key || seen[0].Method != "Put" { t.Fatalf("checker saw %+v, want exactly one Put of %s", seen, key) } if got, _, err := raw.Get(ctx, key); err != nil || !bytes.Equal(got, body) { t.Fatalf("blob not stored: %v", err) } // Create-only: a second put of the same name conflicts in the raw store. if _, err := ps.PutFrom(ctx, key, bytes.NewReader(body), int64(len(body)), ""); !errors.Is(err, release.ErrCASConflict) { t.Errorf("second blob PutFrom: %v, want ErrCASConflict", err) } // A conditional streamed put is not the blob path: it takes the checked, // buffered Put, which classifies the pointer body it was given. seen = nil ptr := geoipTestPointer(t, key, body) if _, err := ps.PutFrom(ctx, geoip.PointerKey, bytes.NewReader(ptr), int64(len(ptr)), ""); err != nil { t.Fatalf("pointer PutFrom: %v", err) } if len(seen) != 1 || seen[0].Key != geoip.PointerKey || !bytes.Equal(seen[0].After, ptr) { t.Errorf("checker saw %+v for a pointer written through PutFrom, want its body", seen) } // An unclassified key never reaches the raw store, however it is spelled. for _, k := range []string{geoip.Prefix + "stray.mmdb", "control/geoip/x/" + strings.Repeat("a", 64) + ".mmdb", "status/other.json"} { seen = nil if _, err := ps.PutFrom(ctx, k, strings.NewReader("x"), 1, ""); err == nil { t.Errorf("PutFrom %s succeeded through the boundary", k) } if _, _, err := raw.Get(ctx, k); !errors.Is(err, os.ErrNotExist) { t.Errorf("PutFrom %s left an object in the raw store: %v", k, err) } } // A blob larger than the pointer allows is refused before any effect. if _, err := ps.PutFrom(ctx, key, strings.NewReader(""), geoip.MaxBlobBytes+1, ""); err == nil { t.Error("an over-limit blob size was accepted") } } // Production has two wrappers between the publisher and the streaming // store. Neither may turn a large deployment blob into a buffered Put. type geoipStreamProbe struct { release.Store streamed bool } func (p *geoipStreamProbe) PutFrom(_ context.Context, _ string, _ io.ReadSeeker, _ int64, _ string) (string, error) { p.streamed = true return "streamed", nil } func TestGeoipStreamingSurvivesRecordBoundary(t *testing.T) { raw := &geoipStreamProbe{Store: release.NewDirStore(t.TempDir())} records, err := NewConditionalRecordStore(raw, make([]byte, 32)) if err != nil { t.Fatal(err) } guarded, err := NewMutationBoundary(true, records, func(ctx context.Context, tg MutationTarget) error { _, err := ClassifyMutation(ctx, records, nil, tg) return err }) if err != nil { t.Fatal(err) } key, _ := geoipTestBlob(t) if _, err = release.PutObjectFrom(t.Context(), guarded, key, bytes.NewReader(nil), release.PutFromFallbackMax+1, ""); err != nil || !raw.streamed { t.Fatalf("streaming lost: %v, raw=%v", err, raw.streamed) } raw.streamed = false if _, err = release.PutObjectFrom(t.Context(), guarded, "apps/bab/demo/data", bytes.NewReader(nil), release.PutFromFallbackMax+1, ""); err == nil || raw.streamed { t.Fatalf("app bypass: %v, raw=%v", err, raw.streamed) } }