this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: add quote client field tests

Verify client fields (client_type, client_network, etc.) are
properly passed through on create, included in list responses,
and correctly omitted from JSON when nil.

+295 -7
+295 -7
internal/handler/api_v1_quotes_test.go
··· 16 16 // mockQuoteStore is a mock implementation of data.Store for testing quote API handlers. 17 17 type mockQuoteStore struct { 18 18 data.Store 19 - quotes []data.Quote 20 - quoteByID *data.Quote 21 - quoteByIDFn func(id int) (*data.Quote, error) 22 - insertedQuoteID int 23 - insertQuoteFn func(quote *data.Quote) (int, error) 24 - deleteQuoteFn func(id int) error 25 - err error 19 + quotes []data.Quote 20 + recentQuotesFn func(filter data.ClientFilter) ([]data.Quote, error) 21 + quoteByID *data.Quote 22 + quoteByIDFn func(id int) (*data.Quote, error) 23 + insertedQuoteID int 24 + insertQuoteFn func(quote *data.Quote) (int, error) 25 + lastInsertedQuote *data.Quote 26 + deleteQuoteFn func(id int) error 27 + err error 26 28 } 27 29 28 30 func (m *mockQuoteStore) GetRecentQuotes(ctx context.Context, days int, offsetDays int, filter data.ClientFilter) ([]data.Quote, error) { 31 + if m.recentQuotesFn != nil { 32 + return m.recentQuotesFn(filter) 33 + } 29 34 if m.err != nil { 30 35 return nil, m.err 31 36 } ··· 43 48 } 44 49 45 50 func (m *mockQuoteStore) InsertQuote(ctx context.Context, quote *data.Quote) (int, error) { 51 + m.lastInsertedQuote = quote 46 52 if m.insertQuoteFn != nil { 47 53 return m.insertQuoteFn(quote) 48 54 } ··· 1018 1024 }) 1019 1025 } 1020 1026 } 1027 + 1028 + func TestAPIv1_CreateQuote_ClientFields(t *testing.T) { 1029 + irc := "irc" 1030 + freenode := "freenode" 1031 + channel := "#general" 1032 + userID := "U12345" 1033 + userName := "alice" 1034 + 1035 + t.Run("client fields passed through to InsertQuote", func(t *testing.T) { 1036 + store := &mockQuoteStore{insertedQuoteID: 50} 1037 + handler := &Handler{ 1038 + Store: store, 1039 + Config: &config.Config{}, 1040 + } 1041 + 1042 + body := `{"quote":"To be or not to be","author":"Shakespeare","poster":"alice","client_type":"irc","client_network":"freenode","client_channel":"#general","client_user_id":"U12345","client_user_name":"alice"}` 1043 + req := httptest.NewRequest(http.MethodPost, "/api/v1/quotes", strings.NewReader(body)) 1044 + req.RemoteAddr = "127.0.0.1:12345" 1045 + req.Header.Set("Content-Type", "application/json") 1046 + w := httptest.NewRecorder() 1047 + 1048 + handler.APIv1QuotesHandler(w, req) 1049 + 1050 + if w.Code != http.StatusCreated { 1051 + t.Fatalf("expected status 201, got %d. Body: %s", w.Code, w.Body.String()) 1052 + } 1053 + 1054 + // Verify client fields were passed to InsertQuote 1055 + inserted := store.lastInsertedQuote 1056 + if inserted == nil { 1057 + t.Fatal("expected InsertQuote to be called") 1058 + } 1059 + if inserted.ClientType == nil || *inserted.ClientType != irc { 1060 + t.Errorf("expected client_type=%q, got %v", irc, inserted.ClientType) 1061 + } 1062 + if inserted.ClientNetwork == nil || *inserted.ClientNetwork != freenode { 1063 + t.Errorf("expected client_network=%q, got %v", freenode, inserted.ClientNetwork) 1064 + } 1065 + if inserted.ClientChannel == nil || *inserted.ClientChannel != channel { 1066 + t.Errorf("expected client_channel=%q, got %v", channel, inserted.ClientChannel) 1067 + } 1068 + if inserted.ClientUserID == nil || *inserted.ClientUserID != userID { 1069 + t.Errorf("expected client_user_id=%q, got %v", userID, inserted.ClientUserID) 1070 + } 1071 + if inserted.ClientUserName == nil || *inserted.ClientUserName != userName { 1072 + t.Errorf("expected client_user_name=%q, got %v", userName, inserted.ClientUserName) 1073 + } 1074 + 1075 + // Verify response contains client fields 1076 + var resp APIQuoteResponse 1077 + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { 1078 + t.Fatalf("failed to unmarshal response: %v", err) 1079 + } 1080 + if resp.ClientType == nil || *resp.ClientType != irc { 1081 + t.Errorf("expected client_type=%q in response, got %v", irc, resp.ClientType) 1082 + } 1083 + if resp.ClientNetwork == nil || *resp.ClientNetwork != freenode { 1084 + t.Errorf("expected client_network=%q in response, got %v", freenode, resp.ClientNetwork) 1085 + } 1086 + if resp.ClientChannel == nil || *resp.ClientChannel != channel { 1087 + t.Errorf("expected client_channel=%q in response, got %v", channel, resp.ClientChannel) 1088 + } 1089 + if resp.ClientUserID == nil || *resp.ClientUserID != userID { 1090 + t.Errorf("expected client_user_id=%q in response, got %v", userID, resp.ClientUserID) 1091 + } 1092 + if resp.ClientUserName == nil || *resp.ClientUserName != userName { 1093 + t.Errorf("expected client_user_name=%q in response, got %v", userName, resp.ClientUserName) 1094 + } 1095 + }) 1096 + } 1097 + 1098 + func TestAPIv1_ListQuotes_ClientFiltering(t *testing.T) { 1099 + now := time.Now() 1100 + irc := "irc" 1101 + slack := "slack" 1102 + freenode := "freenode" 1103 + workspace := "myworkspace" 1104 + chanGeneral := "#general" 1105 + chanRandom := "#random" 1106 + 1107 + allQuotes := []data.Quote{ 1108 + {ID: 1, Timestamp: now, Quote: "IRC Quote", Author: "Author1", Poster: "poster1", 1109 + ClientType: &irc, ClientNetwork: &freenode, ClientChannel: &chanGeneral}, 1110 + {ID: 2, Timestamp: now, Quote: "Slack Quote", Author: "Author2", Poster: "poster2", 1111 + ClientType: &slack, ClientNetwork: &workspace, ClientChannel: &chanRandom}, 1112 + {ID: 3, Timestamp: now, Quote: "No Client Quote", Author: "Author3", Poster: "poster3"}, 1113 + } 1114 + 1115 + t.Run("filter by client_type returns subset", func(t *testing.T) { 1116 + ircOnly := []data.Quote{allQuotes[0]} 1117 + store := &mockQuoteStore{ 1118 + recentQuotesFn: func(filter data.ClientFilter) ([]data.Quote, error) { 1119 + if filter.ClientType != nil && *filter.ClientType == "irc" { 1120 + return ircOnly, nil 1121 + } 1122 + return allQuotes, nil 1123 + }, 1124 + } 1125 + handler := &Handler{ 1126 + Store: store, 1127 + Config: &config.Config{}, 1128 + } 1129 + 1130 + req := httptest.NewRequest(http.MethodGet, "/api/v1/quotes?client_type=irc", nil) 1131 + req.RemoteAddr = "127.0.0.1:12345" 1132 + w := httptest.NewRecorder() 1133 + 1134 + handler.APIv1QuotesHandler(w, req) 1135 + 1136 + if w.Code != http.StatusOK { 1137 + t.Fatalf("expected status 200, got %d", w.Code) 1138 + } 1139 + 1140 + var resp APIQuotesResponse 1141 + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { 1142 + t.Fatalf("failed to unmarshal response: %v", err) 1143 + } 1144 + if len(resp.Data) != 1 { 1145 + t.Fatalf("expected 1 quote, got %d", len(resp.Data)) 1146 + } 1147 + if resp.Data[0].ClientType == nil || *resp.Data[0].ClientType != "irc" { 1148 + t.Errorf("expected client_type=irc, got %v", resp.Data[0].ClientType) 1149 + } 1150 + }) 1151 + 1152 + t.Run("client fields included in list response", func(t *testing.T) { 1153 + store := &mockQuoteStore{quotes: allQuotes} 1154 + handler := &Handler{ 1155 + Store: store, 1156 + Config: &config.Config{}, 1157 + } 1158 + 1159 + req := httptest.NewRequest(http.MethodGet, "/api/v1/quotes", nil) 1160 + req.RemoteAddr = "127.0.0.1:12345" 1161 + w := httptest.NewRecorder() 1162 + 1163 + handler.APIv1QuotesHandler(w, req) 1164 + 1165 + if w.Code != http.StatusOK { 1166 + t.Fatalf("expected status 200, got %d", w.Code) 1167 + } 1168 + 1169 + var resp APIQuotesResponse 1170 + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { 1171 + t.Fatalf("failed to unmarshal response: %v", err) 1172 + } 1173 + 1174 + // First quote should have IRC client fields 1175 + q1 := resp.Data[0] 1176 + if q1.ClientType == nil || *q1.ClientType != "irc" { 1177 + t.Errorf("expected quote 1 client_type=irc, got %v", q1.ClientType) 1178 + } 1179 + if q1.ClientNetwork == nil || *q1.ClientNetwork != "freenode" { 1180 + t.Errorf("expected quote 1 client_network=freenode, got %v", q1.ClientNetwork) 1181 + } 1182 + if q1.ClientChannel == nil || *q1.ClientChannel != "#general" { 1183 + t.Errorf("expected quote 1 client_channel=#general, got %v", q1.ClientChannel) 1184 + } 1185 + 1186 + // Second quote should have Slack client fields 1187 + q2 := resp.Data[1] 1188 + if q2.ClientType == nil || *q2.ClientType != "slack" { 1189 + t.Errorf("expected quote 2 client_type=slack, got %v", q2.ClientType) 1190 + } 1191 + 1192 + // Third quote should have nil client fields 1193 + q3 := resp.Data[2] 1194 + if q3.ClientType != nil { 1195 + t.Errorf("expected quote 3 client_type=nil, got %v", q3.ClientType) 1196 + } 1197 + }) 1198 + } 1199 + 1200 + func TestAPIv1_QuoteResponse_ClientOmitEmpty(t *testing.T) { 1201 + t.Run("null client fields omitted from JSON", func(t *testing.T) { 1202 + now := time.Now() 1203 + store := &mockQuoteStore{ 1204 + quoteByID: &data.Quote{ 1205 + ID: 1, 1206 + Timestamp: now, 1207 + Quote: "To be or not to be", 1208 + Author: "Shakespeare", 1209 + Poster: "testuser", 1210 + }, 1211 + } 1212 + handler := &Handler{ 1213 + Store: store, 1214 + Config: &config.Config{}, 1215 + } 1216 + 1217 + req := httptest.NewRequest(http.MethodGet, "/api/v1/quotes/1", nil) 1218 + req.RemoteAddr = "127.0.0.1:12345" 1219 + w := httptest.NewRecorder() 1220 + 1221 + handler.APIv1QuotesHandler(w, req) 1222 + 1223 + if w.Code != http.StatusOK { 1224 + t.Fatalf("expected status 200, got %d", w.Code) 1225 + } 1226 + 1227 + body := w.Body.String() 1228 + if strings.Contains(body, "client_type") { 1229 + t.Error("expected client_type to be omitted from JSON when nil") 1230 + } 1231 + if strings.Contains(body, "client_network") { 1232 + t.Error("expected client_network to be omitted from JSON when nil") 1233 + } 1234 + if strings.Contains(body, "client_channel") { 1235 + t.Error("expected client_channel to be omitted from JSON when nil") 1236 + } 1237 + if strings.Contains(body, "client_user_id") { 1238 + t.Error("expected client_user_id to be omitted from JSON when nil") 1239 + } 1240 + if strings.Contains(body, "client_user_name") { 1241 + t.Error("expected client_user_name to be omitted from JSON when nil") 1242 + } 1243 + }) 1244 + 1245 + t.Run("set client fields present in JSON", func(t *testing.T) { 1246 + now := time.Now() 1247 + irc := "irc" 1248 + freenode := "freenode" 1249 + channel := "#test" 1250 + userID := "U999" 1251 + userName := "bob" 1252 + 1253 + store := &mockQuoteStore{ 1254 + quoteByID: &data.Quote{ 1255 + ID: 2, 1256 + Timestamp: now, 1257 + Quote: "I think therefore I am", 1258 + Author: "Descartes", 1259 + Poster: "testuser", 1260 + ClientType: &irc, 1261 + ClientNetwork: &freenode, 1262 + ClientChannel: &channel, 1263 + ClientUserID: &userID, 1264 + ClientUserName: &userName, 1265 + }, 1266 + } 1267 + handler := &Handler{ 1268 + Store: store, 1269 + Config: &config.Config{}, 1270 + } 1271 + 1272 + req := httptest.NewRequest(http.MethodGet, "/api/v1/quotes/2", nil) 1273 + req.RemoteAddr = "127.0.0.1:12345" 1274 + w := httptest.NewRecorder() 1275 + 1276 + handler.APIv1QuotesHandler(w, req) 1277 + 1278 + if w.Code != http.StatusOK { 1279 + t.Fatalf("expected status 200, got %d", w.Code) 1280 + } 1281 + 1282 + var resp APIQuoteResponse 1283 + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { 1284 + t.Fatalf("failed to unmarshal response: %v", err) 1285 + } 1286 + 1287 + if resp.ClientType == nil || *resp.ClientType != "irc" { 1288 + t.Errorf("expected client_type=irc, got %v", resp.ClientType) 1289 + } 1290 + if resp.ClientNetwork == nil || *resp.ClientNetwork != "freenode" { 1291 + t.Errorf("expected client_network=freenode, got %v", resp.ClientNetwork) 1292 + } 1293 + if resp.ClientChannel == nil || *resp.ClientChannel != "#test" { 1294 + t.Errorf("expected client_channel=#test, got %v", resp.ClientChannel) 1295 + } 1296 + if resp.ClientUserID == nil || *resp.ClientUserID != "U999" { 1297 + t.Errorf("expected client_user_id=U999, got %v", resp.ClientUserID) 1298 + } 1299 + if resp.ClientUserName == nil || *resp.ClientUserName != "bob" { 1300 + t.Errorf("expected client_user_name=bob, got %v", resp.ClientUserName) 1301 + } 1302 + 1303 + body := w.Body.String() 1304 + if !strings.Contains(body, `"client_type":"irc"`) { 1305 + t.Error("expected client_type in JSON response body") 1306 + } 1307 + }) 1308 + }