mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
457 lines
17 KiB
Go
457 lines
17 KiB
Go
package registry
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
|
|
"github.com/hanzo-fi/payments/pkg/domain/httpwrapper"
|
|
"github.com/hanzo-fi/payments/internal/connectors/plugins"
|
|
"github.com/hanzo-fi/payments/pkg/domain/models"
|
|
"github.com/google/uuid"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
var _ = Describe("Wrapper", func() {
|
|
var (
|
|
ctrl *gomock.Controller
|
|
plg *models.MockPlugin
|
|
connectorID models.ConnectorID
|
|
logger logging.Logger
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
ctrl = gomock.NewController(GinkgoT())
|
|
plg = models.NewMockPlugin(ctrl)
|
|
connectorID = models.ConnectorID{
|
|
Reference: uuid.New(),
|
|
Provider: "psp",
|
|
}
|
|
logger = logging.Testing()
|
|
})
|
|
|
|
Context("install", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.InstallRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().Install(gomock.Any(), req).Return(models.InstallResponse{}, nil)
|
|
_, err := wrapper.Install(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("uninstall", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.UninstallRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().Uninstall(gomock.Any(), req).Return(models.UninstallResponse{}, nil)
|
|
_, err := wrapper.Uninstall(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("fetch next accounts", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.FetchNextAccountsRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().FetchNextAccounts(gomock.Any(), req).Return(models.FetchNextAccountsResponse{}, nil)
|
|
_, err := wrapper.FetchNextAccounts(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("fetch next external accounts", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.FetchNextExternalAccountsRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().FetchNextExternalAccounts(gomock.Any(), req).Return(models.FetchNextExternalAccountsResponse{}, nil)
|
|
_, err := wrapper.FetchNextExternalAccounts(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("fetch next payments", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.FetchNextPaymentsRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().FetchNextPayments(gomock.Any(), req).Return(models.FetchNextPaymentsResponse{}, nil)
|
|
_, err := wrapper.FetchNextPayments(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("fetch next balances", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.FetchNextBalancesRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().FetchNextBalances(gomock.Any(), req).Return(models.FetchNextBalancesResponse{}, nil)
|
|
_, err := wrapper.FetchNextBalances(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("fetch next others", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.FetchNextOthersRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().FetchNextOthers(gomock.Any(), req).Return(models.FetchNextOthersResponse{}, nil)
|
|
_, err := wrapper.FetchNextOthers(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("create bank account", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.CreateBankAccountRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().CreateBankAccount(gomock.Any(), req).Return(models.CreateBankAccountResponse{}, nil)
|
|
_, err := wrapper.CreateBankAccount(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("create transfer", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.CreateTransferRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().CreateTransfer(gomock.Any(), req).Return(models.CreateTransferResponse{}, nil)
|
|
_, err := wrapper.CreateTransfer(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("reverse transfer", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.ReverseTransferRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().ReverseTransfer(gomock.Any(), req).Return(models.ReverseTransferResponse{}, nil)
|
|
_, err := wrapper.ReverseTransfer(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("poll transfer status", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.PollTransferStatusRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().PollTransferStatus(gomock.Any(), req).Return(models.PollTransferStatusResponse{}, nil)
|
|
_, err := wrapper.PollTransferStatus(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("create payout", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.CreatePayoutRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().CreatePayout(gomock.Any(), req).Return(models.CreatePayoutResponse{}, nil)
|
|
_, err := wrapper.CreatePayout(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("reverse payout", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.ReversePayoutRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().ReversePayout(gomock.Any(), req).Return(models.ReversePayoutResponse{}, nil)
|
|
_, err := wrapper.ReversePayout(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("create webhook", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.CreateWebhooksRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().CreateWebhooks(gomock.Any(), req).Return(models.CreateWebhooksResponse{}, nil)
|
|
_, err := wrapper.CreateWebhooks(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("verify webhook", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.VerifyWebhookRequest{Config: &models.WebhookConfig{}}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().VerifyWebhook(gomock.Any(), req).Return(models.VerifyWebhookResponse{}, nil)
|
|
_, err := wrapper.VerifyWebhook(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("translate webhook", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.TranslateWebhookRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().TranslateWebhook(gomock.Any(), req).Return(models.TranslateWebhookResponse{}, nil)
|
|
_, err := wrapper.TranslateWebhook(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("simple delegators", func() {
|
|
It("Name forwards to plugin", func() {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().Name().Return("dummy")
|
|
Expect(wrapper.Name()).To(Equal("dummy"))
|
|
})
|
|
|
|
It("IsScheduledForDeletion forwards to plugin", func() {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().IsScheduledForDeletion().Return(true)
|
|
Expect(wrapper.IsScheduledForDeletion()).To(BeTrue())
|
|
})
|
|
|
|
It("ScheduleForDeletion forwards to plugin", func() {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().ScheduleForDeletion(true)
|
|
wrapper.ScheduleForDeletion(true)
|
|
})
|
|
|
|
It("Config forwards to plugin", func() {
|
|
wrapper := New(connectorID, logger, plg)
|
|
cfg := struct{ Name string }{Name: "c"}
|
|
plg.EXPECT().Config().Return(cfg)
|
|
Expect(wrapper.Config()).To(Equal(cfg))
|
|
})
|
|
})
|
|
|
|
Context("fetch next orders", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.FetchNextOrdersRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().FetchNextOrders(gomock.Any(), req).Return(models.FetchNextOrdersResponse{}, nil)
|
|
_, err := wrapper.FetchNextOrders(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
|
|
It("translates plugin errors", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().FetchNextOrders(gomock.Any(), gomock.Any()).Return(models.FetchNextOrdersResponse{}, plugins.ErrNotImplemented)
|
|
_, err := wrapper.FetchNextOrders(ctx, models.FetchNextOrdersRequest{})
|
|
Expect(errors.Is(err, plugins.ErrNotImplemented)).To(BeTrue())
|
|
})
|
|
})
|
|
|
|
Context("fetch next conversions", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.FetchNextConversionsRequest{}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().FetchNextConversions(gomock.Any(), req).Return(models.FetchNextConversionsResponse{}, nil)
|
|
_, err := wrapper.FetchNextConversions(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
|
|
It("translates plugin errors", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().FetchNextConversions(gomock.Any(), gomock.Any()).Return(models.FetchNextConversionsResponse{}, plugins.ErrNotImplemented)
|
|
_, err := wrapper.FetchNextConversions(ctx, models.FetchNextConversionsRequest{})
|
|
Expect(errors.Is(err, plugins.ErrNotImplemented)).To(BeTrue())
|
|
})
|
|
})
|
|
|
|
Context("poll payout status", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.PollPayoutStatusRequest{PayoutID: "po"}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().PollPayoutStatus(gomock.Any(), req).Return(models.PollPayoutStatusResponse{}, nil)
|
|
_, err := wrapper.PollPayoutStatus(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("trim webhook", func() {
|
|
It("calls underlying function", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.TrimWebhookRequest{Config: &models.WebhookConfig{Name: "n"}}
|
|
plg.EXPECT().Name().Return("dummy").MaxTimes(2)
|
|
plg.EXPECT().TrimWebhook(gomock.Any(), req).Return(models.TrimWebhookResponse{}, nil)
|
|
_, err := wrapper.TrimWebhook(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("user lifecycle", func() {
|
|
It("CreateUser forwards", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.CreateUserRequest{}
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().CreateUser(gomock.Any(), req).Return(models.CreateUserResponse{}, nil)
|
|
_, err := wrapper.CreateUser(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
|
|
It("CreateUserLink forwards", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.CreateUserLinkRequest{}
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().CreateUserLink(gomock.Any(), req).Return(models.CreateUserLinkResponse{}, nil)
|
|
_, err := wrapper.CreateUserLink(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
|
|
It("CompleteUserLink forwards", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.CompleteUserLinkRequest{}
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().CompleteUserLink(gomock.Any(), req).Return(models.CompleteUserLinkResponse{}, nil)
|
|
_, err := wrapper.CompleteUserLink(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
|
|
It("UpdateUserLink forwards", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.UpdateUserLinkRequest{}
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().UpdateUserLink(gomock.Any(), req).Return(models.UpdateUserLinkResponse{}, nil)
|
|
_, err := wrapper.UpdateUserLink(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
|
|
It("CompleteUpdateUserLink forwards", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.CompleteUpdateUserLinkRequest{}
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().CompleteUpdateUserLink(gomock.Any(), req).Return(models.CompleteUpdateUserLinkResponse{}, nil)
|
|
_, err := wrapper.CompleteUpdateUserLink(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
|
|
It("DeleteUserConnection forwards", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.DeleteUserConnectionRequest{}
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().DeleteUserConnection(gomock.Any(), req).Return(models.DeleteUserConnectionResponse{}, nil)
|
|
_, err := wrapper.DeleteUserConnection(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
|
|
It("DeleteUser forwards", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
req := models.DeleteUserRequest{}
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().DeleteUser(gomock.Any(), req).Return(models.DeleteUserResponse{}, nil)
|
|
_, err := wrapper.DeleteUser(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("error translation", func() {
|
|
It("wraps rate-limit errors", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().FetchNextAccounts(gomock.Any(), gomock.Any()).Return(models.FetchNextAccountsResponse{}, httpwrapper.ErrStatusCodeTooManyRequests)
|
|
_, err := wrapper.FetchNextAccounts(ctx, models.FetchNextAccountsRequest{})
|
|
Expect(errors.Is(err, plugins.ErrUpstreamRatelimit)).To(BeTrue())
|
|
})
|
|
|
|
DescribeTable("wraps retryable timeout-class errors as ErrUpstreamTimeout",
|
|
func(ctx SpecContext, sourceErr error) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().FetchNextAccounts(gomock.Any(), gomock.Any()).Return(models.FetchNextAccountsResponse{}, sourceErr)
|
|
_, err := wrapper.FetchNextAccounts(ctx, models.FetchNextAccountsRequest{})
|
|
Expect(errors.Is(err, plugins.ErrUpstreamTimeout)).To(BeTrue())
|
|
},
|
|
Entry("408 Request Timeout", httpwrapper.ErrStatusCodeRequestTimeout),
|
|
Entry("421 Misdirected Request", httpwrapper.ErrStatusCodeMisdirectedRequest),
|
|
)
|
|
|
|
DescribeTable("wraps retry-after-class errors as ErrUpstreamRetryAfter",
|
|
func(ctx SpecContext, sourceErr error) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().FetchNextAccounts(gomock.Any(), gomock.Any()).Return(models.FetchNextAccountsResponse{}, sourceErr)
|
|
_, err := wrapper.FetchNextAccounts(ctx, models.FetchNextAccountsRequest{})
|
|
Expect(errors.Is(err, plugins.ErrUpstreamRetryAfter)).To(BeTrue())
|
|
},
|
|
Entry("423 Locked", httpwrapper.ErrStatusCodeLocked),
|
|
Entry("425 Too Early", httpwrapper.ErrStatusCodeTooEarly),
|
|
)
|
|
|
|
It("wraps invalid-request errors", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
plg.EXPECT().FetchNextPayments(gomock.Any(), gomock.Any()).Return(models.FetchNextPaymentsResponse{}, models.ErrInvalidRequest)
|
|
_, err := wrapper.FetchNextPayments(ctx, models.FetchNextPaymentsRequest{})
|
|
Expect(errors.Is(err, plugins.ErrInvalidClientRequest)).To(BeTrue())
|
|
})
|
|
|
|
It("leaves unknown errors untouched", func(ctx SpecContext) {
|
|
wrapper := New(connectorID, logger, plg)
|
|
plg.EXPECT().Name().Return("dummy").AnyTimes()
|
|
custom := errors.New("custom")
|
|
plg.EXPECT().FetchNextBalances(gomock.Any(), gomock.Any()).Return(models.FetchNextBalancesResponse{}, custom)
|
|
_, err := wrapper.FetchNextBalances(ctx, models.FetchNextBalancesRequest{})
|
|
Expect(err).To(Equal(custom))
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("Wrapper optional upgrades", func() {
|
|
var (
|
|
ctrl *gomock.Controller
|
|
connectorID models.ConnectorID
|
|
logger logging.Logger
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
ctrl = gomock.NewController(GinkgoT())
|
|
connectorID = models.ConnectorID{Reference: uuid.New(), Provider: "psp"}
|
|
logger = logging.Testing()
|
|
})
|
|
|
|
Context("BootstrapOnInstall", func() {
|
|
It("returns nil when plugin does not implement PluginWithBootstrapOnInstall", func() {
|
|
plg := models.NewMockPlugin(ctrl)
|
|
wrapper := New(connectorID, logger, plg)
|
|
Expect(wrapper.BootstrapOnInstall()).To(BeNil())
|
|
})
|
|
|
|
It("forwards when plugin implements PluginWithBootstrapOnInstall", func() {
|
|
plg := &pluginWithBootstrap{
|
|
MockPlugin: models.NewMockPlugin(ctrl),
|
|
inner: models.NewMockPluginWithBootstrapOnInstall(ctrl),
|
|
}
|
|
want := []models.TaskType{models.TASK_FETCH_ACCOUNTS}
|
|
plg.inner.EXPECT().BootstrapOnInstall().Return(want)
|
|
wrapper := New(connectorID, logger, plg)
|
|
Expect(wrapper.BootstrapOnInstall()).To(Equal(want))
|
|
})
|
|
})
|
|
})
|
|
|
|
// pluginWithBootstrap composes MockPlugin with the optional BootstrapOnInstall
|
|
// upgrade so the type assertion inside wrapper.BootstrapOnInstall succeeds.
|
|
type pluginWithBootstrap struct {
|
|
*models.MockPlugin
|
|
inner *models.MockPluginWithBootstrapOnInstall
|
|
}
|
|
|
|
func (p *pluginWithBootstrap) BootstrapOnInstall() []models.TaskType {
|
|
return p.inner.BootstrapOnInstall()
|
|
}
|