Quellcode durchsuchen

Merge pull request #47 from jgknight/fix-missing-chatroom-disconnect

Fix missing chatroom disconnect
Mike vor 2 Jahren
Ursprung
Commit
107c6d335e
4 geänderte Dateien mit 557 neuen und 57 gelöschten Zeilen
  1. 27 0
      README.md
  2. 34 12
      foodgroup/chat_nav.go
  3. 494 41
      foodgroup/chat_nav_test.go
  4. 2 4
      foodgroup/test_helpers.go

+ 27 - 0
README.md

@@ -97,6 +97,21 @@ This request lists sessions for all logged in users.
 Invoke-WebRequest -Uri http://localhost:8080/session -Method Get
 ```
 
+#### Create Public Chat Room
+
+```powershell
+Invoke-WebRequest -Uri http://localhost:8080/chat/room/public `
+  -Body '{"name":"Office Hijinks"}' `
+  -Method Post `
+  -ContentType "application/json"
+```
+
+#### List Public Chat Rooms
+
+```powershell
+Invoke-WebRequest -Uri http://localhost:8080/chat/room/public -Method Get
+```
+
 ### macOS / Linux
 
 #### List Users
@@ -131,6 +146,18 @@ This request lists sessions for all logged in users.
 curl http://localhost:8080/session
 ```
 
+#### Create Public Chat Room
+
+```shell
+curl -d'{"name":"Office Hijinks"}' http://localhost:8080/chat/room/public
+```
+
+#### List Public Chat Rooms
+
+```shell
+curl http://localhost:8080/chat/room/public
+```
+
 ## 🔗 Acknowledgements
 
 - [aim-oscar-server](https://github.com/ox/aim-oscar-server) is another cool open source AIM server project.

+ 34 - 12
foodgroup/chat_nav.go

@@ -24,6 +24,13 @@ var defaultExchangeCfg = wire.TLVBlock{
 	},
 }
 
+var (
+	errChatNavRoomNameMissing    = errors.New("unable to find chat name in TLV payload")
+	errChatNavRoomCreateFailed   = errors.New("unable to create chat room")
+	errChatNavRetrieveFailed     = errors.New("unable to retrieve chat room chat room")
+	errChatNavMismatchedExchange = errors.New("chat room exchange does not match requested exchange")
+)
+
 // NewChatNavService creates a new instance of NewChatNavService.
 func NewChatNavService(logger *slog.Logger, chatRoomManager ChatRoomRegistry, fnNewChatRoom func() state.ChatRoom) *ChatNavService {
 	return &ChatNavService{
@@ -73,7 +80,8 @@ func (s ChatNavService) RequestChatRights(_ context.Context, inFrame wire.SNACFr
 // chat room.
 func (s ChatNavService) CreateRoom(_ context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (wire.SNACMessage, error) {
 	if err := validateExchange(inBody.Exchange); err != nil {
-		return wire.SNACMessage{}, err
+		s.logger.Debug("error validating exchange: " + err.Error())
+		return sendChatNavErrorSNAC(inFrame, wire.ErrorCodeNotSupportedByHost)
 	}
 	if inBody.Cookie != "create" {
 		s.logger.Info("got a non-create cookie", "value", inBody.Cookie)
@@ -81,7 +89,7 @@ func (s ChatNavService) CreateRoom(_ context.Context, sess *state.Session, inFra
 
 	name, hasName := inBody.String(wire.ChatRoomTLVRoomName)
 	if !hasName {
-		return wire.SNACMessage{}, errors.New("unable to find chat name in TLV payload")
+		return wire.SNACMessage{}, errChatNavRoomNameMissing
 	}
 
 	// todo call ChatRoomByName and CreateChatRoom in a txn
@@ -90,8 +98,8 @@ func (s ChatNavService) CreateRoom(_ context.Context, sess *state.Session, inFra
 	switch {
 	case errors.Is(err, state.ErrChatRoomNotFound):
 		if inBody.Exchange == state.PublicExchange {
-			return wire.SNACMessage{}, fmt.Errorf("community chat rooms can only be created on exchange %d",
-				state.PrivateExchange)
+			s.logger.Debug(fmt.Sprintf("public chat room not found: %s:%d", name, inBody.Exchange))
+			return sendChatNavErrorSNAC(inFrame, wire.ErrorCodeNoMatch)
 		}
 
 		room = s.fnNewChatRoom()
@@ -102,14 +110,12 @@ func (s ChatNavService) CreateRoom(_ context.Context, sess *state.Session, inFra
 		room.Name = name
 
 		if err := s.chatRoomManager.CreateChatRoom(room); err != nil {
-			return wire.SNACMessage{}, fmt.Errorf("unable to create chat room: %w", err)
+			return wire.SNACMessage{}, fmt.Errorf("%w: %w", errChatNavRoomCreateFailed, err)
 		}
 		break
 	case err != nil:
-		return wire.SNACMessage{}, fmt.Errorf("unable to retrieve chat room chat room %s on exchange %d: %w",
-			name, inBody.Exchange, err)
+		return wire.SNACMessage{}, fmt.Errorf("%w: %w", errChatNavRetrieveFailed, err)
 	}
-
 	return wire.SNACMessage{
 		Frame: wire.SNACFrame{
 			FoodGroup: wire.ChatNav,
@@ -138,16 +144,17 @@ func (s ChatNavService) CreateRoom(_ context.Context, sess *state.Session, inFra
 // the chat room specified in the inFrame.hmacCookie.
 func (s ChatNavService) RequestRoomInfo(_ context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (wire.SNACMessage, error) {
 	if err := validateExchange(inBody.Exchange); err != nil {
-		return wire.SNACMessage{}, err
+		s.logger.Debug("error validating exchange: " + err.Error())
+		return sendChatNavErrorSNAC(inFrame, wire.ErrorCodeNotSupportedByHost)
 	}
 
 	room, err := s.chatRoomManager.ChatRoomByCookie(inBody.Cookie)
 	if err != nil {
-		return wire.SNACMessage{}, fmt.Errorf("unable to find chat room: %w", err)
+		return wire.SNACMessage{}, fmt.Errorf("%w: %w", state.ErrChatRoomNotFound, err)
 	}
 
 	if room.Exchange != inBody.Exchange {
-		return wire.SNACMessage{}, errors.New("chat room exchange does not match requested exchange")
+		return wire.SNACMessage{}, errChatNavMismatchedExchange
 	}
 
 	return wire.SNACMessage{
@@ -176,7 +183,8 @@ func (s ChatNavService) RequestRoomInfo(_ context.Context, inFrame wire.SNACFram
 
 func (s ChatNavService) ExchangeInfo(_ context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo) (wire.SNACMessage, error) {
 	if err := validateExchange(inBody.Exchange); err != nil {
-		return wire.SNACMessage{}, err
+		s.logger.Debug("error validating exchange: " + err.Error())
+		return sendChatNavErrorSNAC(inFrame, wire.ErrorCodeNotSupportedByHost)
 	}
 	return wire.SNACMessage{
 		Frame: wire.SNACFrame{
@@ -198,6 +206,20 @@ func (s ChatNavService) ExchangeInfo(_ context.Context, inFrame wire.SNACFrame,
 	}, nil
 }
 
+// sendChatNavErrorSNAC returns a ChatNavErr SNAC and logs an error for the operator
+func sendChatNavErrorSNAC(inFrame wire.SNACFrame, errorCode uint16) (wire.SNACMessage, error) {
+	return wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.ChatNav,
+			SubGroup:  wire.ChatNavErr,
+			RequestID: inFrame.RequestID,
+		},
+		Body: wire.SNACError{
+			Code: errorCode,
+		},
+	}, nil
+}
+
 func validateExchange(exchange uint16) error {
 	if !(exchange == state.PrivateExchange || exchange == state.PublicExchange) {
 		return fmt.Errorf("only exchanges %d and %d are supported", state.PrivateExchange, state.PublicExchange)

+ 494 - 41
foodgroup/chat_nav_test.go

@@ -2,6 +2,7 @@ package foodgroup
 
 import (
 	"context"
+	"errors"
 	"log/slog"
 	"testing"
 	"time"
@@ -22,6 +23,15 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 		InstanceNumber: 2,
 		Name:           "the-chat-room-name",
 	}
+	publicChatRoom := state.ChatRoom{
+		Cookie:         "dummy-cookie",
+		CreateTime:     time.UnixMilli(0),
+		Creator:        state.NewIdentScreenName("the-screen-name"),
+		DetailLevel:    3,
+		Exchange:       5,
+		InstanceNumber: 2,
+		Name:           "the-public-chat-room-name",
+	}
 
 	tests := []struct {
 		name          string
@@ -34,7 +44,7 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 		fnNewChatRoom func() state.ChatRoom
 	}{
 		{
-			name:     "create room that already exists",
+			name:     "create private room that already exists",
 			chatRoom: basicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
@@ -91,7 +101,7 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 			},
 		},
 		{
-			name:     "create room that doesn't already exist",
+			name:     "create private room that doesn't already exist",
 			chatRoom: basicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
@@ -145,10 +155,242 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 						},
 					},
 					createChatRoomParams: createChatRoomParams{
+						{
+							room: basicChatRoom,
+						},
+					},
+				},
+			},
+			fnNewChatRoom: func() state.ChatRoom {
+				return basicChatRoom
+			},
+		},
+		{
+			name:     "create public room that already exists",
+			chatRoom: publicChatRoom,
+			sess:     newTestSession("the-screen-name"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+					Exchange:       publicChatRoom.Exchange,
+					Cookie:         "create", // actual canned value sent by AIM client
+					InstanceNumber: publicChatRoom.InstanceNumber,
+					DetailLevel:    publicChatRoom.DetailLevel,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.ChatRoomTLVRoomName, publicChatRoom.Name),
+						},
+					},
+				},
+			},
+			want: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavNavInfo,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(
+								wire.ChatNavRequestRoomInfo,
+								wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+									Exchange:       publicChatRoom.Exchange,
+									Cookie:         publicChatRoom.Cookie,
+									InstanceNumber: publicChatRoom.InstanceNumber,
+									DetailLevel:    publicChatRoom.DetailLevel,
+									TLVBlock: wire.TLVBlock{
+										TLVList: publicChatRoom.TLVList(),
+									},
+								},
+							),
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				chatRoomRegistryParams: chatRoomRegistryParams{
+					chatRoomByNameParams: chatRoomByNameParams{
+						{
+							exchange: publicChatRoom.Exchange,
+							name:     publicChatRoom.Name,
+							room:     publicChatRoom,
+						},
+					},
+				},
+			},
+		},
+		{
+			name:     "create public room that does not exist",
+			chatRoom: publicChatRoom,
+			sess:     newTestSession("the-screen-name"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+					Exchange:       publicChatRoom.Exchange,
+					Cookie:         "create", // actual canned value sent by AIM client
+					InstanceNumber: publicChatRoom.InstanceNumber,
+					DetailLevel:    publicChatRoom.DetailLevel,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.ChatRoomTLVRoomName, publicChatRoom.Name),
+						},
+					},
+				},
+			},
+			want: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavErr,
+					RequestID: 1234,
+				},
+				Body: wire.SNACError{
+					Code: wire.ErrorCodeNoMatch,
+				},
+			},
+			mockParams: mockParams{
+				chatRoomRegistryParams: chatRoomRegistryParams{
+					chatRoomByNameParams: chatRoomByNameParams{
+						{
+							exchange: publicChatRoom.Exchange,
+							name:     publicChatRoom.Name,
+							err:      state.ErrChatRoomNotFound,
+						},
+					},
+				},
+			},
+			fnNewChatRoom: func() state.ChatRoom {
+				return basicChatRoom
+			},
+		},
+		{
+			name:     "create room invalid exchange number",
+			chatRoom: basicChatRoom,
+			sess:     newTestSession("the-screen-name"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+					Exchange:       1337,
+					Cookie:         "create", // actual canned value sent by AIM client
+					InstanceNumber: basicChatRoom.InstanceNumber,
+					DetailLevel:    basicChatRoom.DetailLevel,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name),
+						},
+					},
+				},
+			},
+			want: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavErr,
+					RequestID: 1234,
+				},
+				Body: wire.SNACError{
+					Code: wire.ErrorCodeNotSupportedByHost,
+				},
+			},
+		},
+		{
+			name:     "incoming create room missing name tlv",
+			chatRoom: basicChatRoom,
+			sess:     newTestSession("the-screen-name"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+					Exchange:       basicChatRoom.Exchange,
+					Cookie:         "create", // actual canned value sent by AIM client
+					InstanceNumber: basicChatRoom.InstanceNumber,
+					DetailLevel:    basicChatRoom.DetailLevel,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{}, // intentionally empty for test
+					},
+				},
+			},
+			want:    wire.SNACMessage{},
+			wantErr: errChatNavRoomNameMissing,
+		},
+		{
+			name:     "create private room failed",
+			chatRoom: basicChatRoom,
+			sess:     newTestSession("the-screen-name"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+					Exchange:       basicChatRoom.Exchange,
+					Cookie:         "create", // actual canned value sent by AIM client
+					InstanceNumber: basicChatRoom.InstanceNumber,
+					DetailLevel:    basicChatRoom.DetailLevel,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name),
+						},
+					},
+				},
+			},
+			want:    wire.SNACMessage{},
+			wantErr: errChatNavRoomCreateFailed,
+			mockParams: mockParams{
+				chatRoomRegistryParams: chatRoomRegistryParams{
+					chatRoomByNameParams: chatRoomByNameParams{
+						{
+							exchange: basicChatRoom.Exchange,
+							name:     basicChatRoom.Name,
+							err:      state.ErrChatRoomNotFound,
+						},
+					},
+					createChatRoomParams: createChatRoomParams{
+						{
+							room: basicChatRoom,
+							err:  errors.New("fake database error"),
+						},
+					},
+				},
+			},
+			fnNewChatRoom: func() state.ChatRoom {
+				return basicChatRoom
+			},
+		},
+		{
+			name:     "create private room failed unknown error",
+			chatRoom: basicChatRoom,
+			sess:     newTestSession("the-screen-name"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+					Exchange:       basicChatRoom.Exchange,
+					Cookie:         "create", // actual canned value sent by AIM client
+					InstanceNumber: basicChatRoom.InstanceNumber,
+					DetailLevel:    basicChatRoom.DetailLevel,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name),
+						},
+					},
+				},
+			},
+			want:    wire.SNACMessage{},
+			wantErr: errChatNavRetrieveFailed,
+			mockParams: mockParams{
+				chatRoomRegistryParams: chatRoomRegistryParams{
+					chatRoomByNameParams: chatRoomByNameParams{
 						{
 							exchange: basicChatRoom.Exchange,
 							name:     basicChatRoom.Name,
-							room:     basicChatRoom,
+							err:      errors.New("fake database error"),
 						},
 					},
 				},
@@ -174,10 +416,8 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 			}
 
 			svc := NewChatNavService(slog.Default(), chatRoomRegistry, tt.fnNewChatRoom)
-
 			outputSNAC, err := svc.CreateRoom(context.Background(), tt.sess, tt.inputSNAC.Frame, tt.inputSNAC.Body.(wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate))
-			assert.NoError(t, err)
-
+			assert.ErrorIs(t, err, tt.wantErr)
 			assert.Equal(t, tt.want, outputSNAC)
 		})
 	}
@@ -192,7 +432,7 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 		wantErr    error
 	}{
 		{
-			name: "request room info",
+			name: "request existing private room info successfully",
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
@@ -240,6 +480,136 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "request existing public room info successfully",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
+					Exchange: state.PublicExchange,
+					Cookie:   "the-chat-cookie",
+				},
+			},
+			want: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavNavInfo,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(0x04, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+								Cookie:         "the-chat-cookie",
+								DetailLevel:    2,
+								Exchange:       state.PublicExchange,
+								InstanceNumber: 8,
+								TLVBlock: wire.TLVBlock{
+									TLVList: state.ChatRoom{Cookie: "the-chat-cookie"}.TLVList(),
+								},
+							}),
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				chatRoomRegistryParams: chatRoomRegistryParams{
+					chatRoomByCookieParams: chatRoomByCookieParams{
+						{
+							cookie: "the-chat-cookie",
+							room: state.ChatRoom{
+								Cookie:         "the-chat-cookie",
+								DetailLevel:    2,
+								Exchange:       state.PublicExchange,
+								InstanceNumber: 8,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "request room info with invalid exchange number",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
+					Exchange: 1337,
+					Cookie:   "the-chat-cookie",
+				},
+			},
+			want: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavErr,
+					RequestID: 1234,
+				},
+				Body: wire.SNACError{
+					Code: wire.ErrorCodeNotSupportedByHost,
+				},
+			},
+		},
+		{
+			name: "request room info on nonexistent room",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
+					Exchange: state.PrivateExchange,
+					Cookie:   "the-chat-cookie",
+				},
+			},
+			want:    wire.SNACMessage{},
+			wantErr: errChatNavMismatchedExchange,
+			mockParams: mockParams{
+				chatRoomRegistryParams: chatRoomRegistryParams{
+					chatRoomByCookieParams: chatRoomByCookieParams{
+						{
+							cookie: "the-chat-cookie",
+							room: state.ChatRoom{
+								Cookie:         "the-chat-cookie",
+								DetailLevel:    2,
+								Exchange:       state.PublicExchange,
+								InstanceNumber: 8,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "request room info with mismatched exchange",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
+					Exchange: state.PrivateExchange,
+					Cookie:   "the-chat-cookie",
+				},
+			},
+			want:    wire.SNACMessage{},
+			wantErr: state.ErrChatRoomNotFound,
+			mockParams: mockParams{
+				chatRoomRegistryParams: chatRoomRegistryParams{
+					chatRoomByCookieParams: chatRoomByCookieParams{
+						{
+							cookie: "the-chat-cookie",
+							room: state.ChatRoom{
+								Cookie:         "the-chat-cookie",
+								DetailLevel:    2,
+								Exchange:       state.PrivateExchange,
+								InstanceNumber: 8,
+							},
+							err: state.ErrChatRoomNotFound,
+						},
+					},
+				},
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -318,45 +688,128 @@ func TestChatNavService_RequestChatRights(t *testing.T) {
 }
 
 func TestChatNavService_ExchangeInfo(t *testing.T) {
-	svc := NewChatNavService(nil, nil, nil)
-
-	frame := wire.SNACFrame{RequestID: 1234}
-	snac := wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo{
-		Exchange: 4,
-	}
-	have, err := svc.ExchangeInfo(nil, frame, snac)
-	assert.NoError(t, err)
-
-	want := wire.SNACMessage{
-		Frame: wire.SNACFrame{
-			FoodGroup: wire.ChatNav,
-			SubGroup:  wire.ChatNavNavInfo,
-			RequestID: frame.RequestID,
+	tests := []struct {
+		name       string
+		inputSNAC  wire.SNACMessage
+		want       wire.SNACMessage
+		mockParams mockParams
+		wantErr    error
+	}{
+		{
+			name: "request private exchange info",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo{
+					Exchange: state.PrivateExchange,
+				},
+			},
+			want: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavNavInfo,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.ChatNavTLVMaxConcurrentRooms, uint8(10)),
+							wire.NewTLV(wire.ChatNavTLVExchangeInfo, wire.SNAC_0x0D_0x09_TLVExchangeInfo{
+								Identifier: state.PrivateExchange,
+								TLVBlock: wire.TLVBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLV(wire.ChatRoomTLVMaxConcurrentRooms, uint8(10)),
+										wire.NewTLV(wire.ChatRoomTLVClassPerms, uint16(0x0010)),
+										wire.NewTLV(wire.ChatRoomTLVMaxNameLen, uint16(100)),
+										wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
+										wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
+										wire.NewTLV(wire.ChatRoomTLVCharSet1, "us-ascii"),
+										wire.NewTLV(wire.ChatRoomTLVLang1, "en"),
+										wire.NewTLV(wire.ChatRoomTLVCharSet2, "us-ascii"),
+										wire.NewTLV(wire.ChatRoomTLVLang2, "en"),
+									},
+								},
+							}),
+						},
+					},
+				},
+			},
 		},
-		Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
-			TLVRestBlock: wire.TLVRestBlock{
-				TLVList: wire.TLVList{
-					wire.NewTLV(wire.ChatNavTLVMaxConcurrentRooms, uint8(10)),
-					wire.NewTLV(wire.ChatNavTLVExchangeInfo, wire.SNAC_0x0D_0x09_TLVExchangeInfo{
-						Identifier: snac.Exchange,
-						TLVBlock: wire.TLVBlock{
-							TLVList: wire.TLVList{
-								wire.NewTLV(wire.ChatRoomTLVMaxConcurrentRooms, uint8(10)),
-								wire.NewTLV(wire.ChatRoomTLVClassPerms, uint16(0x0010)),
-								wire.NewTLV(wire.ChatRoomTLVMaxNameLen, uint16(100)),
-								wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
-								wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
-								wire.NewTLV(wire.ChatRoomTLVCharSet1, "us-ascii"),
-								wire.NewTLV(wire.ChatRoomTLVLang1, "en"),
-								wire.NewTLV(wire.ChatRoomTLVCharSet2, "us-ascii"),
-								wire.NewTLV(wire.ChatRoomTLVLang2, "en"),
-							},
+		{
+			name: "request public exchange info",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo{
+					Exchange: state.PublicExchange,
+				},
+			},
+			want: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavNavInfo,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.ChatNavTLVMaxConcurrentRooms, uint8(10)),
+							wire.NewTLV(wire.ChatNavTLVExchangeInfo, wire.SNAC_0x0D_0x09_TLVExchangeInfo{
+								Identifier: state.PublicExchange,
+								TLVBlock: wire.TLVBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLV(wire.ChatRoomTLVMaxConcurrentRooms, uint8(10)),
+										wire.NewTLV(wire.ChatRoomTLVClassPerms, uint16(0x0010)),
+										wire.NewTLV(wire.ChatRoomTLVMaxNameLen, uint16(100)),
+										wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
+										wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
+										wire.NewTLV(wire.ChatRoomTLVCharSet1, "us-ascii"),
+										wire.NewTLV(wire.ChatRoomTLVLang1, "en"),
+										wire.NewTLV(wire.ChatRoomTLVCharSet2, "us-ascii"),
+										wire.NewTLV(wire.ChatRoomTLVLang2, "en"),
+									},
+								},
+							}),
 						},
-					}),
+					},
+				},
+			},
+		},
+		{
+			name: "request invalid exchange info",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo{
+					Exchange: 6,
+				},
+			},
+			want: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavErr,
+					RequestID: 1234,
+				},
+				Body: wire.SNACError{
+					Code: wire.ErrorCodeNotSupportedByHost,
 				},
 			},
 		},
 	}
 
-	assert.Equal(t, want, have)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			svc := NewChatNavService(slog.Default(), nil, nil)
+			outputSNAC, err := svc.ExchangeInfo(context.Background(), tt.inputSNAC.Frame,
+				tt.inputSNAC.Body.(wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo))
+			assert.ErrorIs(t, err, tt.wantErr)
+			if tt.wantErr != nil {
+				return
+			}
+			assert.Equal(t, tt.want, outputSNAC)
+		})
+	}
 }

+ 2 - 4
foodgroup/test_helpers.go

@@ -378,10 +378,8 @@ type chatRoomByNameParams []struct {
 // createChatRoomParams is the list of parameters passed at the mock
 // ChatRoomRegistry.CreateChatRoom call site
 type createChatRoomParams []struct {
-	exchange uint16
-	name     string
-	room     state.ChatRoom
-	err      error
+	room state.ChatRoom
+	err  error
 }
 
 // sessOptWarning sets a warning level on the session object