Skip to content

Commit

Permalink
feat(#193): support load testing
Browse files Browse the repository at this point in the history
  • Loading branch information
Jumpy-Squirrel committed Jan 21, 2024
1 parent fd006e3 commit c3a47d1
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 13 deletions.
10 changes: 10 additions & 0 deletions cmd/loadtest/loadtest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import (
"github.com/eurofurence/reg-attendee-service/internal/web/app"
"os"
)

func main() {
os.Exit(app.New().Loadtest())
}
2 changes: 2 additions & 0 deletions docs/config-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ security:
allow_origin: 'http://localhost:8000'
# set this to true to require a valid oidc token for initial reg. This allow the service to store the subject of the token and use it for authorization
require_login_for_reg: true
# set this to true temporarily to use the load testing command with constant tokens. Never use in production.
# anonymize_identity: true
logging:
severity: INFO
style: plain # or ecs (elastic common schema), the default
Expand Down
26 changes: 25 additions & 1 deletion internal/repository/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,30 @@ func UseEcsLogging() bool {
return ecsLogging
}

func GenerateCount() uint {
func GeneratorGenerateCount() uint {
return generateCount
}

func GeneratorParallel() uint {
return parallel
}

func GeneratorBaseUrl() string {
return baseUrl
}

func GeneratorCookieDomain() string {
return cookieDomain
}

func GeneratorIdToken() string {
return idToken
}

func GeneratorAccessToken() string {
return accessToken
}

func ServerAddr() string {
c := Configuration()
return fmt.Sprintf("%s:%s", c.Server.Address, c.Server.Port)
Expand Down Expand Up @@ -269,6 +289,10 @@ func RequireLoginForReg() bool {
return Configuration().Security.RequireLogin
}

func AnonymizeIdentity() bool {
return Configuration().Security.AnonymizeIdentity
}

func PaymentServiceBaseUrl() string {
return Configuration().Service.PaymentService
}
Expand Down
18 changes: 16 additions & 2 deletions internal/repository/config/loading.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ var (
configurationFilename string
dbMigrate bool
ecsLogging bool
generateCount uint

generateCount uint
parallel uint
baseUrl string
cookieDomain string
idToken string
accessToken string

parsedKeySet []*rsa.PublicKey
)
Expand All @@ -36,7 +42,15 @@ func init() {
flag.StringVar(&configurationFilename, "config", "", "config file path")
flag.BoolVar(&dbMigrate, "migrate-database", false, "migrate database on startup")
flag.BoolVar(&ecsLogging, "ecs-json-logging", false, "switch to structured json logging")
flag.UintVar(&generateCount, "generate-count", 0, "number of fake registrations to generate (separate generator binary only)")
}

func AdditionalGeneratorCommandLineFlags() {
flag.UintVar(&generateCount, "generate-count", 0, "total number of fake registrations to generate (separate generator/loadtest binaries only)")
flag.UintVar(&parallel, "parallel", 0, "number of parallel goroutines to use (separate generator/loadtest binaries only)")
flag.StringVar(&baseUrl, "base-url", "", "base url of target attendee service (separate generator/loadtest binaries only)")
flag.StringVar(&cookieDomain, "cookie-domain", "", "domain for cookies (separate generator/loadtest binaries only)")
flag.StringVar(&idToken, "id-token", "", "id token to use (separate generator/loadtest binaries only)")
flag.StringVar(&accessToken, "access-token", "", "access token to use (separate generator/loadtest binaries only)")
}

// ParseCommandLineFlags is exposed separately so you can skip it for tests
Expand Down
9 changes: 5 additions & 4 deletions internal/repository/config/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ type (

// SecurityConfig configures everything related to security
SecurityConfig struct {
Fixed FixedTokenConfig `yaml:"fixed_token"`
Oidc OpenIdConnectConfig `yaml:"oidc"`
Cors CorsConfig `yaml:"cors"`
RequireLogin bool `yaml:"require_login_for_reg"`
Fixed FixedTokenConfig `yaml:"fixed_token"`
Oidc OpenIdConnectConfig `yaml:"oidc"`
Cors CorsConfig `yaml:"cors"`
RequireLogin bool `yaml:"require_login_for_reg"`
AnonymizeIdentity bool `yaml:"anonymize_identity"`
}

FixedTokenConfig struct {
Expand Down
103 changes: 103 additions & 0 deletions internal/repository/selfclient/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Package selfclient provides a very simple self API client.
package selfclient

import (
"context"
"errors"
"fmt"
aurestclientapi "github.com/StephanHCB/go-autumn-restclient/api"
auresthttpclient "github.com/StephanHCB/go-autumn-restclient/implementation/httpclient"
aurestlogging "github.com/StephanHCB/go-autumn-restclient/implementation/requestlogging"
"github.com/eurofurence/reg-attendee-service/internal/api/v1/attendee"
"github.com/eurofurence/reg-attendee-service/internal/repository/config"
"github.com/go-http-utils/headers"
"net/http"
"strings"
"time"
)

var (
client aurestclientapi.Client
baseUrl string
idToken string
accessToken string
cookieDomain string
)

var (
UnauthorizedError = errors.New("got unauthorized from userinfo endpoint")
DownstreamError = errors.New("downstream unavailable - see log for details")
)

func requestManipulator(ctx context.Context, r *http.Request) {
r.AddCookie(&http.Cookie{
Name: config.OidcIdTokenCookieName(),
Value: idToken,
Domain: cookieDomain,
Expires: time.Now().Add(10 * time.Minute),
Path: "/",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
r.AddCookie(&http.Cookie{
Name: config.OidcAccessTokenCookieName(),
Value: accessToken,
Domain: cookieDomain,
Expires: time.Now().Add(10 * time.Minute),
Path: "/",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
}

func Setup() error {
httpClient, err := auresthttpclient.New(0, nil, requestManipulator)
if err != nil {
return err
}

requestLoggingClient := aurestlogging.New(httpClient)

client = requestLoggingClient

baseUrl = config.GeneratorBaseUrl()
idToken = config.GeneratorIdToken()
accessToken = config.GeneratorAccessToken()
cookieDomain = config.GeneratorCookieDomain()

if baseUrl == "" || idToken == "" || accessToken == "" || cookieDomain == "" {
return errors.New("missing parameters for setting up selfclient - cannot run generator")
}

return nil
}

func errByStatus(err error, status int) error {
if err != nil {
return err
}
if status == http.StatusUnauthorized {
return UnauthorizedError
}
if status >= 300 {
return DownstreamError
}
return nil
}

func SendRegistration(ctx context.Context, dto *attendee.AttendeeDto) (string, error) {
url := fmt.Sprintf("%s/api/rest/v1/attendees", baseUrl)
response := aurestclientapi.ParsedResponse{}
err := client.Perform(ctx, http.MethodPost, url, dto, &response)

// parse location header
loc := ""
if val, ok := response.Header[headers.Location]; ok {
loc = val[0]
}
id := strings.TrimPrefix(loc, url+"/")

return id, errByStatus(err, response.Status)
}
9 changes: 7 additions & 2 deletions internal/service/attendeesrv/attendeesrv.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ func (s *AttendeeServiceImplData) RegisterNewAttendee(ctx context.Context, atten
return 0, errors.New("duplicate attendee data - you are already registered")
}

// record which user owns this attendee
attendee.Identity = ctxvalues.Subject(ctx)
if config.AnonymizeIdentity() {
// used for testing with generated fake regs
attendee.Identity = randomString(10, 12, 0) + "_gen"
} else {
// record which user owns this attendee
attendee.Identity = ctxvalues.Subject(ctx)
}

if config.RequireLoginForReg() {
alreadyHasRegistration, err := userAlreadyHasAnotherRegistration(ctx, attendee.Identity, 0)
Expand Down
46 changes: 43 additions & 3 deletions internal/service/attendeesrv/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"fmt"
aulogging "github.com/StephanHCB/go-autumn-logging"
"github.com/eurofurence/reg-attendee-service/internal/api/v1/attendee"
"github.com/eurofurence/reg-attendee-service/internal/entity"
"github.com/eurofurence/reg-attendee-service/internal/repository/config"
"github.com/eurofurence/reg-attendee-service/internal/repository/database"
"github.com/eurofurence/reg-attendee-service/internal/repository/selfclient"
"math/rand"
"strings"
"time"
Expand All @@ -26,6 +28,17 @@ func (s *AttendeeServiceImplData) GenerateFakeRegistrations(ctx context.Context,
return nil
}

func (s *AttendeeServiceImplData) SendFakeRegistrationToAPI(ctx context.Context) (string, error) {
a := fakeRegistration()
dto := &attendee.AttendeeDto{}
mapAttendeeToDto(a, dto)

ctxTimeout, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()

return selfclient.SendRegistration(ctxTimeout, dto)
}

func fakeRegistration() *entity.Attendee {
return &entity.Attendee{
Nickname: randomString(3, 40, 2),
Expand All @@ -34,7 +47,7 @@ func fakeRegistration() *entity.Attendee {
Street: randomString(3, 120, 0),
Zip: randomString(3, 15, 0),
City: randomString(3, 80, 0),
Country: "",
Country: oneOf(config.AllowedCountries()),
Email: "[email protected]",
Phone: "12345",
Birthday: randomValidBirthday(),
Expand All @@ -43,9 +56,9 @@ func fakeRegistration() *entity.Attendee {
SpokenLanguages: "," + randomSelection(config.AllowedSpokenLanguages(), 1, 5) + ",",
RegistrationLanguage: oneOf(config.AllowedRegistrationLanguages()),
Flags: ",terms-accepted," + randomSelection([]string{"hc", "anon", "digi-book"}, 1, 3) + ",",
Packages: ",room-none,attendance," + oneOf([]string{"sponsor", "sponsor2", "tshirt"}) + ",",
Packages: ",room-none,attendance,stage," + oneOf([]string{"sponsor", "sponsor2", "tshirt"}) + ",",
Options: "," + randomSelection(config.AllowedOptions(), 1, 4) + ",",
UserComments: "",
UserComments: "generated by load test",
Identity: randomString(10, 12, 0) + "_gen",
}
}
Expand Down Expand Up @@ -132,3 +145,30 @@ func safeRandIntn(len int) int {
return 0
}
}

func mapAttendeeToDto(a *entity.Attendee, dto *attendee.AttendeeDto) {
// this cannot fail
dto.Id = a.ID
dto.Nickname = a.Nickname
dto.FirstName = a.FirstName
dto.LastName = a.LastName
dto.Street = a.Street
dto.Zip = a.Zip
dto.City = a.City
dto.Country = a.Country
dto.State = a.State
dto.Email = a.Email
dto.Phone = a.Phone
dto.Telegram = a.Telegram
dto.Partner = a.Partner
dto.Birthday = a.Birthday
dto.Gender = a.Gender
dto.Pronouns = a.Pronouns
dto.TshirtSize = a.TshirtSize
dto.SpokenLanguages = removeWrappingCommas(a.SpokenLanguages)
dto.RegistrationLanguage = removeWrappingCommas(a.RegistrationLanguage)
dto.Flags = removeWrappingCommas(a.Flags)
dto.Packages = removeWrappingCommas(a.Packages)
dto.Options = removeWrappingCommas(a.Options)
dto.UserComments = a.UserComments
}
7 changes: 7 additions & 0 deletions internal/service/attendeesrv/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ type AttendeeService interface {
//
// Only for use on test systems.
GenerateFakeRegistrations(ctx context.Context, count uint) error

// SendFakeRegistrationToAPI sends a fake registration via the API.
//
// Only for use on test systems.
//
// Must configure identity_anonymize on the receiver.
SendFakeRegistrationToAPI(ctx context.Context) (string, error)
}

var (
Expand Down
Loading

0 comments on commit c3a47d1

Please sign in to comment.