chore: send admin email on adding waitlist entry
This commit is contained in:
@@ -76,7 +76,7 @@ func main() {
|
||||
GoogleServiceAccountEmail: utils.MustGetEnv("GOOGLE_SERVICE_ACCOUNT_EMAIL"),
|
||||
BucketName: gcsBucket,
|
||||
})
|
||||
waitlistSvc := waitlist.NewService(db.Pool())
|
||||
waitlistSvc := waitlist.NewService(db.Pool(), aeroSvc)
|
||||
|
||||
// Initialize handler
|
||||
h := handler.NewHandler(authSvc, humanSvc, networkSvc, particleSvc, depotSvc, waitlistSvc)
|
||||
@@ -135,7 +135,7 @@ func main() {
|
||||
mux.Handle("POST /waitlist/invite", withAuth(h.InviteWaitlistEntrant))
|
||||
|
||||
// Apply middleware
|
||||
// nil allows all origins (required for electron app)
|
||||
// nil allows all origins (required for electron app)
|
||||
muxWithCors := middleware.CORS(nil)(mux)
|
||||
|
||||
addr := fmt.Sprintf("0.0.0.0:%s", port)
|
||||
|
||||
@@ -103,6 +103,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
|
||||
@@ -244,6 +244,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
|
||||
@@ -42,8 +42,8 @@ func NewHandler(authSvc auth.AuthService, humanSvc human.Service, networkSvc net
|
||||
|
||||
type Human struct {
|
||||
Id string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
EmailPrefix string `json:"email_prefix"`
|
||||
Email string `json:"email"`
|
||||
EmailPrefix string `json:"email_prefix"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: /Users/talksik/Documents/code/llink/go/genproto/aero/main_grpc.pb.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source=/Users/talksik/Documents/code/llink/go/genproto/aero/main_grpc.pb.go -destination=/Users/talksik/Documents/code/llink/go/internal/waitlist/mocks/mock_aero.go -package=mocks
|
||||
//
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
pbaero "github.com/flowy-live/llink/genproto/aero"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
grpc "google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// MockPrimaryClient is a mock of PrimaryClient interface.
|
||||
type MockPrimaryClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockPrimaryClientMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockPrimaryClientMockRecorder is the mock recorder for MockPrimaryClient.
|
||||
type MockPrimaryClientMockRecorder struct {
|
||||
mock *MockPrimaryClient
|
||||
}
|
||||
|
||||
// NewMockPrimaryClient creates a new mock instance.
|
||||
func NewMockPrimaryClient(ctrl *gomock.Controller) *MockPrimaryClient {
|
||||
mock := &MockPrimaryClient{ctrl: ctrl}
|
||||
mock.recorder = &MockPrimaryClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockPrimaryClient) EXPECT() *MockPrimaryClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ShootEmail mocks base method.
|
||||
func (m *MockPrimaryClient) ShootEmail(ctx context.Context, in *pbaero.ShootEmailRequest, opts ...grpc.CallOption) (*pbaero.ShootEmailResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, in}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "ShootEmail", varargs...)
|
||||
ret0, _ := ret[0].(*pbaero.ShootEmailResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ShootEmail indicates an expected call of ShootEmail.
|
||||
func (mr *MockPrimaryClientMockRecorder) ShootEmail(ctx, in any, opts ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx, in}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShootEmail", reflect.TypeOf((*MockPrimaryClient)(nil).ShootEmail), varargs...)
|
||||
}
|
||||
|
||||
// MockPrimaryServer is a mock of PrimaryServer interface.
|
||||
type MockPrimaryServer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockPrimaryServerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockPrimaryServerMockRecorder is the mock recorder for MockPrimaryServer.
|
||||
type MockPrimaryServerMockRecorder struct {
|
||||
mock *MockPrimaryServer
|
||||
}
|
||||
|
||||
// NewMockPrimaryServer creates a new mock instance.
|
||||
func NewMockPrimaryServer(ctrl *gomock.Controller) *MockPrimaryServer {
|
||||
mock := &MockPrimaryServer{ctrl: ctrl}
|
||||
mock.recorder = &MockPrimaryServerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockPrimaryServer) EXPECT() *MockPrimaryServerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ShootEmail mocks base method.
|
||||
func (m *MockPrimaryServer) ShootEmail(arg0 context.Context, arg1 *pbaero.ShootEmailRequest) (*pbaero.ShootEmailResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ShootEmail", arg0, arg1)
|
||||
ret0, _ := ret[0].(*pbaero.ShootEmailResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ShootEmail indicates an expected call of ShootEmail.
|
||||
func (mr *MockPrimaryServerMockRecorder) ShootEmail(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShootEmail", reflect.TypeOf((*MockPrimaryServer)(nil).ShootEmail), arg0, arg1)
|
||||
}
|
||||
|
||||
// mustEmbedUnimplementedPrimaryServer mocks base method.
|
||||
func (m *MockPrimaryServer) mustEmbedUnimplementedPrimaryServer() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "mustEmbedUnimplementedPrimaryServer")
|
||||
}
|
||||
|
||||
// mustEmbedUnimplementedPrimaryServer indicates an expected call of mustEmbedUnimplementedPrimaryServer.
|
||||
func (mr *MockPrimaryServerMockRecorder) mustEmbedUnimplementedPrimaryServer() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedPrimaryServer", reflect.TypeOf((*MockPrimaryServer)(nil).mustEmbedUnimplementedPrimaryServer))
|
||||
}
|
||||
|
||||
// MockUnsafePrimaryServer is a mock of UnsafePrimaryServer interface.
|
||||
type MockUnsafePrimaryServer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockUnsafePrimaryServerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockUnsafePrimaryServerMockRecorder is the mock recorder for MockUnsafePrimaryServer.
|
||||
type MockUnsafePrimaryServerMockRecorder struct {
|
||||
mock *MockUnsafePrimaryServer
|
||||
}
|
||||
|
||||
// NewMockUnsafePrimaryServer creates a new mock instance.
|
||||
func NewMockUnsafePrimaryServer(ctrl *gomock.Controller) *MockUnsafePrimaryServer {
|
||||
mock := &MockUnsafePrimaryServer{ctrl: ctrl}
|
||||
mock.recorder = &MockUnsafePrimaryServerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockUnsafePrimaryServer) EXPECT() *MockUnsafePrimaryServerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// mustEmbedUnimplementedPrimaryServer mocks base method.
|
||||
func (m *MockUnsafePrimaryServer) mustEmbedUnimplementedPrimaryServer() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "mustEmbedUnimplementedPrimaryServer")
|
||||
}
|
||||
|
||||
// mustEmbedUnimplementedPrimaryServer indicates an expected call of mustEmbedUnimplementedPrimaryServer.
|
||||
func (mr *MockUnsafePrimaryServerMockRecorder) mustEmbedUnimplementedPrimaryServer() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedPrimaryServer", reflect.TypeOf((*MockUnsafePrimaryServer)(nil).mustEmbedUnimplementedPrimaryServer))
|
||||
}
|
||||
@@ -3,8 +3,11 @@ package waitlist
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
pbaero "github.com/flowy-live/llink/genproto/aero"
|
||||
"github.com/flowy-live/llink/internal/utils"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
@@ -41,12 +44,14 @@ type Service interface {
|
||||
}
|
||||
|
||||
type serviceImpl struct {
|
||||
repo repository
|
||||
repo repository
|
||||
aeroSvc pbaero.PrimaryClient
|
||||
}
|
||||
|
||||
func NewService(pool *pgxpool.Pool) Service {
|
||||
func NewService(pool *pgxpool.Pool, aeroSvc pbaero.PrimaryClient) Service {
|
||||
return &serviceImpl{
|
||||
repo: newRepository(pool),
|
||||
repo: newRepository(pool),
|
||||
aeroSvc: aeroSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +73,20 @@ func (s *serviceImpl) AddToWaitlist(ctx context.Context, email string, metadata
|
||||
return err
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("New human added to the llink waitlist: %s", email)
|
||||
_, err = s.aeroSvc.ShootEmail(context.Background(), &pbaero.ShootEmailRequest{
|
||||
ToEmails: []string{"team@flowylabs.ai"},
|
||||
Subject: "Flowy Llink - New Waitlist Subscriber",
|
||||
TemplateData: &pbaero.ShootEmailRequest_GenericFlowyAdminAlertData{
|
||||
GenericFlowyAdminAlertData: &pbaero.GenericFlowyAdminAlertData{
|
||||
Message: message,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("failed to add to waitlist", "error", err, "email", email)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
pbaero "github.com/flowy-live/llink/genproto/aero"
|
||||
"github.com/flowy-live/llink/internal/testhelper"
|
||||
"github.com/flowy-live/llink/internal/waitlist"
|
||||
"github.com/flowy-live/llink/internal/waitlist/mocks"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
var dbPool *pgxpool.Pool
|
||||
@@ -21,9 +24,19 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(ret)
|
||||
}
|
||||
|
||||
func newTestService(t *testing.T) waitlist.Service {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockAero := mocks.NewMockPrimaryClient(ctrl)
|
||||
mockAero.EXPECT().
|
||||
ShootEmail(gomock.Any(), gomock.Any()).
|
||||
Return(&pbaero.ShootEmailResponse{}, nil).
|
||||
AnyTimes()
|
||||
return waitlist.NewService(dbPool, mockAero)
|
||||
}
|
||||
|
||||
func TestWaitlistService_AddAndGet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := waitlist.NewService(dbPool)
|
||||
svc := newTestService(t)
|
||||
|
||||
email := "newuser@example.com"
|
||||
metadata := map[string]string{"source": "landing-page", "plan": "pro"}
|
||||
@@ -48,7 +61,7 @@ func TestWaitlistService_AddAndGet(t *testing.T) {
|
||||
|
||||
func TestWaitlistService_AddDuplicate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := waitlist.NewService(dbPool)
|
||||
svc := newTestService(t)
|
||||
|
||||
email := "duplicate@example.com"
|
||||
|
||||
@@ -66,7 +79,7 @@ func TestWaitlistService_AddDuplicate(t *testing.T) {
|
||||
|
||||
func TestWaitlistService_AddNilMetadata(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := waitlist.NewService(dbPool)
|
||||
svc := newTestService(t)
|
||||
|
||||
email := "nilmeta@example.com"
|
||||
|
||||
@@ -84,7 +97,7 @@ func TestWaitlistService_AddNilMetadata(t *testing.T) {
|
||||
|
||||
func TestWaitlistService_GetEntryNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := waitlist.NewService(dbPool)
|
||||
svc := newTestService(t)
|
||||
|
||||
_, err := svc.GetWaitlistEntryByEmail(ctx, "nonexistent@example.com")
|
||||
assert.ErrorIs(t, err, waitlist.EntryNotFoundError)
|
||||
@@ -92,7 +105,7 @@ func TestWaitlistService_GetEntryNotFound(t *testing.T) {
|
||||
|
||||
func TestWaitlistService_MarkInvited(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := waitlist.NewService(dbPool)
|
||||
svc := newTestService(t)
|
||||
|
||||
email := "invite-me@example.com"
|
||||
|
||||
@@ -115,7 +128,7 @@ func TestWaitlistService_MarkInvited(t *testing.T) {
|
||||
|
||||
func TestWaitlistService_MarkInvitedNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := waitlist.NewService(dbPool)
|
||||
svc := newTestService(t)
|
||||
|
||||
err := svc.MarkWaitlistEntryInvited(ctx, "ghost@example.com")
|
||||
assert.ErrorIs(t, err, waitlist.EntryNotFoundError)
|
||||
@@ -123,7 +136,7 @@ func TestWaitlistService_MarkInvitedNotFound(t *testing.T) {
|
||||
|
||||
func TestWaitlistService_GetWaitlistFilters(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := waitlist.NewService(dbPool)
|
||||
svc := newTestService(t)
|
||||
|
||||
uninvitedEmail := "uninvited-filter@example.com"
|
||||
invitedEmail := "invited-filter@example.com"
|
||||
@@ -166,7 +179,7 @@ func TestWaitlistService_GetWaitlistFilters(t *testing.T) {
|
||||
|
||||
func TestWaitlistService_EmailNormalization(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := waitlist.NewService(dbPool)
|
||||
svc := newTestService(t)
|
||||
|
||||
// Add with uppercase email
|
||||
err := svc.AddToWaitlist(ctx, "UPPER@EXAMPLE.COM", nil)
|
||||
@@ -188,7 +201,7 @@ func TestWaitlistService_EmailNormalization(t *testing.T) {
|
||||
|
||||
func TestWaitlistService_InvalidEmail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := waitlist.NewService(dbPool)
|
||||
svc := newTestService(t)
|
||||
|
||||
err := svc.AddToWaitlist(ctx, "not-an-email", nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
Reference in New Issue
Block a user