| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- package webapi
- import (
- "context"
- "fmt"
- "log/slog"
- "testing"
- "time"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- // newTestIconSource returns a BuddyIconSource whose users have no buddy icon,
- // for tests that are not exercising icons.
- func newTestIconSource(t *testing.T) BuddyIconSource {
- iconRetriever := newMockBuddyIconRetriever(t)
- iconRetriever.EXPECT().BuddyIconMetadata(mock.Anything, mock.Anything).Return(nil, nil).Maybe()
- return BuddyIconSource{
- IconRetriever: iconRetriever,
- BARTService: newMockBARTService(t),
- Logger: slog.Default(),
- }
- }
- // tightRateLimitClasses returns rate classes scaled down so tests run fast.
- //
- // OSCAR's moving average tracks the interval between requests in milliseconds,
- // seeded at MaxLevel, and each back-to-back request halves it at WindowSize 2.
- // So from 200 the sequence is 100 (clear), 50 (limited), 25, 12, 6 — the second
- // request trips the limit, and none of the first five fall below the disconnect
- // threshold. Recovering past ClearLevel takes a ~150ms pause rather than the
- // several seconds the production classes would need.
- func tightRateLimitClasses() wire.RateLimitClasses {
- var classes [5]wire.RateClass
- for i := range classes {
- classes[i] = wire.RateClass{
- ID: wire.RateLimitClassID(i + 1),
- WindowSize: 2,
- ClearLevel: 100,
- AlertLevel: 80,
- LimitLevel: 70,
- DisconnectLevel: 2,
- MaxLevel: 200,
- }
- }
- return wire.NewRateLimitClasses(classes)
- }
- // newTestOSCARInstance builds an OSCAR session with rate limit state
- // initialized, mirroring what RegisterBOSSession does at startSession time.
- func newTestOSCARInstance(t *testing.T, classes wire.RateLimitClasses) *state.SessionInstance {
- t.Helper()
- instance := state.NewSession().AddInstance()
- instance.Session().SetIdentScreenName(state.NewIdentScreenName("me"))
- instance.Session().SetDisplayScreenName("me")
- instance.Session().SetRateClasses(time.Now(), classes)
- return instance
- }
- // newTestWebAPISessionOn builds a WebAPI session over an existing OSCAR
- // instance. Two of them model two browser tabs signed in as the same account:
- // each tab holds its own aimsid and its own Session, but the account has
- // one OSCAR session and therefore one set of rate limit states.
- func newTestWebAPISessionOn(aimsid string, instance *state.SessionInstance) *Session {
- return &Session{
- AimSID: aimsid,
- ScreenName: "me",
- OSCARSession: instance,
- EventQueue: NewEventQueue(10),
- FeedbagLoader: emptyFeedbagLoader,
- }
- }
- // emptyFeedbagLoader stands in for the loader startSession wires, for sessions
- // whose test does not care what is on the roster.
- func emptyFeedbagLoader(context.Context) ([]wire.FeedbagItem, error) {
- return nil, nil
- }
- // newTestWebAPISession builds a WebAPI session backed by a real OSCAR session
- // with rate limit state initialized.
- func newTestWebAPISession(t *testing.T, classes wire.RateLimitClasses) *Session {
- t.Helper()
- return newTestWebAPISessionOn("aimsid-1", newTestOSCARInstance(t, classes))
- }
- // rateLimitEventStatuses returns the status string of every rateLimit event
- // queued on the session, in order.
- func rateLimitEventStatuses(t *testing.T, session *Session) []string {
- t.Helper()
- var statuses []string
- for _, event := range session.EventQueue.GetAllEvents() {
- if event.Type != EventTypeRateLimit {
- continue
- }
- payload, ok := event.Data.(RateLimitEvent)
- if !assert.True(t, ok, "rateLimit event carried %T", event.Data) {
- continue
- }
- if assert.Len(t, payload.Classes, 1) {
- statuses = append(statuses, payload.Classes[0].Status)
- }
- }
- return statuses
- }
- // buddyArrives feeds a BuddyArrived through the session's SNAC handler, which is
- // how its presence view learns that a buddy is online.
- func buddyArrives(sess *Session, info wire.TLVUserInfo) {
- sess.handleBuddyArrived(wire.SNACMessage{Body: wire.SNAC_0x03_0x0B_BuddyArrived{TLVUserInfo: info}})
- }
- // buddyDeparts feeds a BuddyDeparted through the session's SNAC handler.
- func buddyDeparts(sess *Session, screenName string) {
- sess.handleBuddyDeparted(wire.SNACMessage{Body: wire.SNAC_0x03_0x0C_BuddyDeparted{
- TLVUserInfo: wire.TLVUserInfo{ScreenName: screenName},
- }})
- }
- // onlineBuddy is the user info an ordinary online buddy arrives with.
- func onlineBuddy(screenName string) wire.TLVUserInfo {
- return wire.TLVUserInfo{ScreenName: screenName}
- }
- // bartBuddy is the user info a buddy arrives with when they advertise BART
- // items, which carry the icon hash and the status message.
- func bartBuddy(screenName string, ids ...wire.BARTID) wire.TLVUserInfo {
- info := wire.TLVUserInfo{ScreenName: screenName}
- info.Append(wire.NewTLVBE(wire.OServiceUserInfoBARTInfo, ids))
- return info
- }
- // aliasFeedbagLoader returns a FeedbagLoader serving one buddy item per alias.
- func aliasFeedbagLoader(aliases map[string]string) func(context.Context) ([]wire.FeedbagItem, error) {
- return func(context.Context) ([]wire.FeedbagItem, error) {
- return aliasFeedbagItems(aliases), nil
- }
- }
- // aliasFeedbagItems builds the buddy rows carrying the given aliases.
- func aliasFeedbagItems(aliases map[string]string) []wire.FeedbagItem {
- items := make([]wire.FeedbagItem, 0, len(aliases))
- var itemID uint16
- for name, alias := range aliases {
- itemID++
- item := wire.FeedbagItem{ItemID: itemID, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: name}
- item.TLVLBlock = wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesAlias, alias)}}
- items = append(items, item)
- }
- return items
- }
- // feedbagServiceLoader returns a FeedbagLoader backed by a FeedbagService, as
- // startSession wires it.
- func feedbagServiceLoader(fs FeedbagService, instance *state.SessionInstance) func(context.Context) ([]wire.FeedbagItem, error) {
- return func(ctx context.Context) ([]wire.FeedbagItem, error) {
- snac, err := fs.Query(ctx, instance, wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery})
- if err != nil {
- return nil, err
- }
- reply, ok := snac.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
- if !ok {
- return nil, fmt.Errorf("unexpected feedbag reply type")
- }
- return reply.Items, nil
- }
- }
|