package toc
import (
"encoding/base64"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang.org/x/time/rate"
"github.com/mk6i/open-oscar-server/state"
"github.com/mk6i/open-oscar-server/wire"
)
func TestOSCARProxy_NewServeMux(t *testing.T) {
cookieBaker, err := state.NewHMACCookieBaker()
assert.NoError(t, err)
p := OSCARProxy{
CookieBaker: cookieBaker,
Logger: slog.Default(),
}
cookie, err := p.newHTTPAuthToken(state.NewIdentScreenName("me"))
assert.NoError(t, err)
cases := []struct {
// name is the unit test name
name string
// path is the HTTP request path
path string
// expectedStatus is the expected HTTP response code
expectedStatus int
// expectedBody is the expected body payload
expectedBody string
// mockParams is the list of params sent to mocks that satisfy this
// method's dependencies
mockParams mockParams
}{
{
name: "Successfully retrieve HTML profile",
path: "/info?from=me&user=them&cookie=" + cookie,
expectedStatus: http.StatusOK,
expectedBody: `Let's chat
colorfg colorbg big bold italic underline 8-)
strikesubsup`,
mockParams: mockParams{
locateParams: locateParams{
userInfoQueryParams: userInfoQueryParams{
{
me: state.NewIdentScreenName("me"),
inBody: wire.SNAC_0x02_0x05_LocateUserInfoQuery{
Type: uint16(wire.LocateTypeSig),
ScreenName: "them",
},
msg: wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Locate,
SubGroup: wire.LocateUserInfoReply,
},
Body: wire.SNAC_0x02_0x06_LocateUserInfoReply{
TLVUserInfo: newTestSession("them").Session().TLVUserInfo(),
LocateInfo: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, `"Let's chat
colorfg colorbg big bold italic underline 8-)
strikesubsup"`),
},
},
},
},
},
},
},
sessionRetrieverParams: sessionRetrieverParams{
retrieveSessionParams: retrieveSessionParams{
{
screenName: state.NewIdentScreenName("me"),
returnedSession: newTestSession("me").Session(),
},
},
},
},
},
{
name: "Successfully retrieve plaintext profile",
path: "/info?from=me&user=them&cookie=" + cookie,
expectedStatus: http.StatusOK,
expectedBody: "My profile!",
mockParams: mockParams{
locateParams: locateParams{
userInfoQueryParams: userInfoQueryParams{
{
me: state.NewIdentScreenName("me"),
inBody: wire.SNAC_0x02_0x05_LocateUserInfoQuery{
Type: uint16(wire.LocateTypeSig),
ScreenName: "them",
},
msg: wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Locate,
SubGroup: wire.LocateUserInfoReply,
},
Body: wire.SNAC_0x02_0x06_LocateUserInfoReply{
TLVUserInfo: newTestSession("them").Session().TLVUserInfo(),
LocateInfo: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, "My profile!"),
},
},
},
},
},
},
},
sessionRetrieverParams: sessionRetrieverParams{
retrieveSessionParams: retrieveSessionParams{
{
screenName: state.NewIdentScreenName("me"),
returnedSession: newTestSession("me").Session(),
},
},
},
},
},
{
name: "Retrieve profile with missing `from` query param",
path: "/info?user=them&cookie=" + cookie,
expectedStatus: http.StatusBadRequest,
expectedBody: "required `from` param is missing",
},
{
name: "Retrieve profile with missing `from` query param",
path: "/info?from=me&cookie=" + cookie,
expectedStatus: http.StatusBadRequest,
expectedBody: "required `user` param is missing",
},
{
name: "Retrieve profile with missing `cookie` query param",
path: "/info?from=me&user=them",
expectedStatus: http.StatusBadRequest,
expectedBody: "required `cookie` param is missing",
},
{
name: "Retrieve profile with invalid auth cookie",
path: func() string {
return "/info?from=me&user=them&cookie=" + base64.URLEncoding.EncodeToString([]byte("blahblah"))
}(),
expectedStatus: http.StatusForbidden,
expectedBody: "invalid auth cookie",
},
{
name: "Retrieve profile, receive error from locate svc",
path: "/info?from=me&user=them&cookie=" + cookie,
expectedStatus: http.StatusInternalServerError,
expectedBody: "internal server error",
mockParams: mockParams{
locateParams: locateParams{
userInfoQueryParams: userInfoQueryParams{
{
me: state.NewIdentScreenName("me"),
inBody: wire.SNAC_0x02_0x05_LocateUserInfoQuery{
Type: uint16(wire.LocateTypeSig),
ScreenName: "them",
},
msg: wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Locate,
SubGroup: wire.LocateUserInfoReply,
},
Body: wire.SNAC_0x02_0x06_LocateUserInfoReply{
TLVUserInfo: newTestSession("them").Session().TLVUserInfo(),
LocateInfo: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, "My profile!"),
},
},
},
},
err: io.EOF,
},
},
},
sessionRetrieverParams: sessionRetrieverParams{
retrieveSessionParams: retrieveSessionParams{
{
screenName: state.NewIdentScreenName("me"),
returnedSession: newTestSession("me").Session(),
},
},
},
},
},
{
name: "Retrieve profile, receive unknown response from locate svc",
path: "/info?from=me&user=them&cookie=" + cookie,
expectedStatus: http.StatusInternalServerError,
expectedBody: "internal server error",
mockParams: mockParams{
locateParams: locateParams{
userInfoQueryParams: userInfoQueryParams{
{
me: state.NewIdentScreenName("me"),
inBody: wire.SNAC_0x02_0x05_LocateUserInfoQuery{
Type: uint16(wire.LocateTypeSig),
ScreenName: "them",
},
msg: wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Locate,
SubGroup: wire.LocateUserInfoReply,
},
Body: wire.SNAC_0x04_0x09_ICBMEvilReply{},
},
},
},
},
sessionRetrieverParams: sessionRetrieverParams{
retrieveSessionParams: retrieveSessionParams{
{
screenName: state.NewIdentScreenName("me"),
returnedSession: newTestSession("me").Session(),
},
},
},
},
},
{
name: "Retrieve profile, user offline",
path: "/info?from=me&user=them&cookie=" + cookie,
expectedStatus: http.StatusNotFound,
expectedBody: "user is unavailable",
mockParams: mockParams{
locateParams: locateParams{
userInfoQueryParams: userInfoQueryParams{
{
me: state.NewIdentScreenName("me"),
inBody: wire.SNAC_0x02_0x05_LocateUserInfoQuery{
Type: uint16(wire.LocateTypeSig),
ScreenName: "them",
},
msg: wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Locate,
SubGroup: wire.LocateUserInfoReply,
},
Body: wire.SNACError{
Code: wire.ErrorCodeNotLoggedOn,
},
},
},
},
},
sessionRetrieverParams: sessionRetrieverParams{
retrieveSessionParams: retrieveSessionParams{
{
screenName: state.NewIdentScreenName("me"),
returnedSession: newTestSession("me").Session(),
},
},
},
},
},
{
name: "Retrieve profile, receive error code from locate svc",
path: "/info?from=me&user=them&cookie=" + cookie,
expectedStatus: http.StatusInternalServerError,
expectedBody: "internal server error",
mockParams: mockParams{
locateParams: locateParams{
userInfoQueryParams: userInfoQueryParams{
{
me: state.NewIdentScreenName("me"),
inBody: wire.SNAC_0x02_0x05_LocateUserInfoQuery{
Type: uint16(wire.LocateTypeSig),
ScreenName: "them",
},
msg: wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.Locate,
SubGroup: wire.LocateUserInfoReply,
},
Body: wire.SNACError{
Code: wire.ErrorCodeInvalidSnac,
},
},
},
},
},
sessionRetrieverParams: sessionRetrieverParams{
retrieveSessionParams: retrieveSessionParams{
{
screenName: state.NewIdentScreenName("me"),
returnedSession: newTestSession("me").Session(),
},
},
},
},
},
{
name: "Successfully retrieve directory info",
path: "/dir_info?user=them&cookie=" + cookie,
expectedStatus: http.StatusOK,
expectedBody: "their_first_name",
mockParams: mockParams{
locateParams: locateParams{
dirInfoParams: dirInfoParams{
{
body: wire.SNAC_0x02_0x0B_LocateGetDirInfo{
ScreenName: "them",
},
msg: wire.SNACMessage{
Body: wire.SNAC_0x02_0x0C_LocateGetDirReply{
Status: wire.LocateGetDirReplyOK,
TLVBlock: wire.TLVBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVFirstName, "their_first_name"),
},
},
},
},
},
},
},
},
},
{
name: "Retrieve directory info, no results found",
path: "/dir_info?user=them&cookie=" + cookie,
expectedStatus: http.StatusNotFound,
expectedBody: "no user directory info found",
mockParams: mockParams{
locateParams: locateParams{
dirInfoParams: dirInfoParams{
{
body: wire.SNAC_0x02_0x0B_LocateGetDirInfo{
ScreenName: "them",
},
msg: wire.SNACMessage{
Body: wire.SNAC_0x02_0x0C_LocateGetDirReply{
Status: wire.LocateGetDirReplyOK,
TLVBlock: wire.TLVBlock{
TLVList: wire.TLVList{},
},
},
},
},
},
},
},
},
{
name: "Retrieve directory info, unexpected response from locate svc",
path: "/dir_info?user=them&cookie=" + cookie,
expectedStatus: http.StatusInternalServerError,
expectedBody: "internal server error",
mockParams: mockParams{
locateParams: locateParams{
dirInfoParams: dirInfoParams{
{
body: wire.SNAC_0x02_0x0B_LocateGetDirInfo{
ScreenName: "them",
},
msg: wire.SNACMessage{
Body: wire.SNACError{},
},
},
},
},
},
},
{
name: "Search directory with missing `user` query param",
path: "/dir_info?cookie=" + cookie,
expectedStatus: http.StatusBadRequest,
expectedBody: "required `user` param is missing",
},
{
name: "Retrieve directory info, receive err from locate svc",
path: "/dir_info?user=them&cookie=" + cookie,
expectedStatus: http.StatusInternalServerError,
expectedBody: "internal server error",
mockParams: mockParams{
locateParams: locateParams{
dirInfoParams: dirInfoParams{
{
body: wire.SNAC_0x02_0x0B_LocateGetDirInfo{
ScreenName: "them",
},
msg: wire.SNACMessage{
Body: wire.SNAC_0x02_0x0C_LocateGetDirReply{
Status: wire.LocateGetDirReplyOK,
TLVBlock: wire.TLVBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVFirstName, "their_first_name"),
},
},
},
},
err: io.EOF,
},
},
},
},
},
{
name: "Successfully search directory by name and address",
path: "/dir_search?first_name=their_first_name" +
"&middle_name=their_middle_name" +
"&last_name=their_last_name" +
"&maiden_name=their_maiden_name" +
"&city=their_city" +
"&state=their_state" +
"&country=their_country" +
"&cookie=" + cookie,
expectedStatus: http.StatusOK,
expectedBody: "their_first_name",
mockParams: mockParams{
dirSearchParams: dirSearchParams{
infoQueryParams: infoQueryParams{
{
inBody: wire.SNAC_0x0F_0x02_InfoQuery{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVFirstName, "their_first_name"),
wire.NewTLVBE(wire.ODirTLVMiddleName, "their_middle_name"),
wire.NewTLVBE(wire.ODirTLVLastName, "their_last_name"),
wire.NewTLVBE(wire.ODirTLVMaidenName, "their_maiden_name"),
wire.NewTLVBE(wire.ODirTLVCity, "their_city"),
wire.NewTLVBE(wire.ODirTLVState, "their_state"),
wire.NewTLVBE(wire.ODirTLVCountry, "their_country"),
},
},
},
msg: wire.SNACMessage{
Body: wire.SNAC_0x0F_0x03_InfoReply{
Status: wire.ODirSearchResponseOK,
Results: struct {
List []wire.TLVBlock `oscar:"count_prefix=uint16"`
}{
List: []wire.TLVBlock{
{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVFirstName, "their_first_name"),
},
},
},
},
},
},
},
},
},
},
},
{
name: "Successfully search directory by email",
path: "/dir_search?email=their_email@aol.com&cookie=" + cookie,
expectedStatus: http.StatusOK,
expectedBody: "their_first_name",
mockParams: mockParams{
dirSearchParams: dirSearchParams{
infoQueryParams: infoQueryParams{
{
inBody: wire.SNAC_0x0F_0x02_InfoQuery{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVEmailAddress, "their_email@aol.com"),
},
},
},
msg: wire.SNACMessage{
Body: wire.SNAC_0x0F_0x03_InfoReply{
Status: wire.ODirSearchResponseOK,
Results: struct {
List []wire.TLVBlock `oscar:"count_prefix=uint16"`
}{
List: []wire.TLVBlock{
{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVFirstName, "their_first_name"),
},
},
},
},
},
},
},
},
},
},
},
{
name: "Successfully search directory by keyword",
path: "/dir_search?keyword=their_keyword&cookie=" + cookie,
expectedStatus: http.StatusOK,
expectedBody: "their_first_name",
mockParams: mockParams{
dirSearchParams: dirSearchParams{
infoQueryParams: infoQueryParams{
{
inBody: wire.SNAC_0x0F_0x02_InfoQuery{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVInterest, "their_keyword"),
},
},
},
msg: wire.SNACMessage{
Body: wire.SNAC_0x0F_0x03_InfoReply{
Status: wire.ODirSearchResponseOK,
Results: struct {
List []wire.TLVBlock `oscar:"count_prefix=uint16"`
}{
List: []wire.TLVBlock{
{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVFirstName, "their_first_name"),
},
},
},
},
},
},
},
},
},
},
},
{
name: "Search directory by email, receive err from dir search svc",
path: "/dir_search?email=their_email@aol.com&cookie=" + cookie,
expectedStatus: http.StatusInternalServerError,
expectedBody: "internal server error",
mockParams: mockParams{
dirSearchParams: dirSearchParams{
infoQueryParams: infoQueryParams{
{
inBody: wire.SNAC_0x0F_0x02_InfoQuery{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVEmailAddress, "their_email@aol.com"),
},
},
},
msg: wire.SNACMessage{
Body: wire.SNAC_0x0F_0x03_InfoReply{
Status: wire.ODirSearchResponseOK,
Results: struct {
List []wire.TLVBlock `oscar:"count_prefix=uint16"`
}{
List: []wire.TLVBlock{
{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVFirstName, "their_first_name"),
},
},
},
},
},
},
err: io.EOF,
},
},
},
},
},
{
name: "Search directory by email, receive unknown response from dir search svc",
path: "/dir_search?email=their_email@aol.com&cookie=" + cookie,
expectedStatus: http.StatusInternalServerError,
expectedBody: "internal server error",
mockParams: mockParams{
dirSearchParams: dirSearchParams{
infoQueryParams: infoQueryParams{
{
inBody: wire.SNAC_0x0F_0x02_InfoQuery{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLVBE(wire.ODirTLVEmailAddress, "their_email@aol.com"),
},
},
},
msg: wire.SNACMessage{
Body: wire.SNACError{},
},
},
},
},
},
},
{
name: "Search directory without any correct search parameters",
path: "/dir_search?cookie=" + cookie,
expectedStatus: http.StatusBadRequest,
expectedBody: "missing search parameters",
mockParams: mockParams{
dirSearchParams: dirSearchParams{
infoQueryParams: infoQueryParams{
{
inBody: wire.SNAC_0x0F_0x02_InfoQuery{},
msg: wire.SNACMessage{
Body: wire.SNAC_0x0F_0x03_InfoReply{
Status: wire.ODirSearchResponseNameMissing,
},
},
},
},
},
},
},
{
name: "Search directory, receive error code from search dir svc",
path: "/dir_search?cookie=" + cookie,
expectedStatus: http.StatusInternalServerError,
expectedBody: "internal server error",
mockParams: mockParams{
dirSearchParams: dirSearchParams{
infoQueryParams: infoQueryParams{
{
inBody: wire.SNAC_0x0F_0x02_InfoQuery{},
msg: wire.SNACMessage{
Body: wire.SNAC_0x0F_0x03_InfoReply{
Status: wire.ODirSearchResponseUnavailable1,
},
},
},
},
},
},
},
{
name: "Search directory, receive unknown response from search dir svc",
path: "/dir_search?cookie=" + cookie,
expectedStatus: http.StatusInternalServerError,
expectedBody: "internal server error",
mockParams: mockParams{
dirSearchParams: dirSearchParams{
infoQueryParams: infoQueryParams{
{
inBody: wire.SNAC_0x0F_0x02_InfoQuery{},
msg: wire.SNACMessage{
Body: wire.SNAC_0x0F_0x03_InfoReply{
Status: wire.ODirSearchResponseUnavailable1,
},
},
},
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
locateSvc := newMockLocateService(t)
for _, params := range tc.mockParams.userInfoQueryParams {
locateSvc.EXPECT().
UserInfoQuery(mock.Anything, matchSession(params.me), wire.SNACFrame{}, params.inBody).
Return(params.msg, params.err)
}
for _, params := range tc.mockParams.dirInfoParams {
locateSvc.EXPECT().
DirInfo(mock.Anything, wire.SNACFrame{}, params.body).
Return(params.msg, params.err)
}
dirSearchSvc := newMockDirSearchService(t)
for _, params := range tc.mockParams.infoQueryParams {
dirSearchSvc.EXPECT().
InfoQuery(mock.Anything, wire.SNACFrame{}, params.inBody).
Return(params.msg, params.err)
}
sessionRetriever := newMockSessionRetriever(t)
for _, params := range tc.mockParams.retrieveSessionParams {
sessionRetriever.EXPECT().
RetrieveSession(params.screenName).
Return(params.returnedSession)
}
svc := OSCARProxy{
CookieBaker: cookieBaker,
DirSearchService: dirSearchSvc,
LocateService: locateSvc,
Logger: slog.Default(),
SessionRetriever: sessionRetriever,
HTTPIPRateLimiter: NewIPRateLimiter(rate.Every(1*time.Minute), 10, 1*time.Minute),
SNACRateLimits: wire.DefaultSNACRateLimits(),
}
req, err := http.NewRequest(http.MethodGet, tc.path, nil)
req.RemoteAddr = "127.0.0.1:1234"
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
rr := httptest.NewRecorder()
svc.NewServeMux().ServeHTTP(rr, req)
assert.Equal(t, tc.expectedStatus, rr.Code)
assert.Contains(t, rr.Body.String(), tc.expectedBody)
})
}
}