this repo has no description
1
fork

Configure Feed

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

feat: Can now get a random quote at /quote

+236
+66
internal/assets/openapi.json
··· 129 129 } 130 130 }, 131 131 "/quote/": { 132 + "get": { 133 + "summary": "Get a Random Quote", 134 + "responses": { 135 + "200": { 136 + "description": "Random Quote", 137 + "content": { 138 + "text/plain": { 139 + "schema": { 140 + "type": "string", 141 + "example": "Wise words -- Author" 142 + } 143 + }, 144 + "text/html": { 145 + "schema": { 146 + "type": "string" 147 + } 148 + }, 149 + "application/json": { 150 + "schema": { 151 + "type": "object", 152 + "properties": { 153 + "quoteID": { "type": "integer" }, 154 + "timestamp": { "type": "string", "format": "date-time" }, 155 + "quote": { "type": "string" }, 156 + "author": { "type": "string" } 157 + } 158 + } 159 + } 160 + } 161 + } 162 + } 163 + }, 132 164 "post": { 133 165 "summary": "Submit a Quote", 134 166 "requestBody": { ··· 180 212 "responses": { 181 213 "200": { 182 214 "description": "Search Results HTML", 215 + "content": { 216 + "text/html": { 217 + "schema": { 218 + "type": "string" 219 + } 220 + } 221 + } 222 + } 223 + } 224 + } 225 + }, 226 + "/stats": { 227 + "get": { 228 + "summary": "Get User Statistics", 229 + "parameters": [ 230 + { 231 + "name": "page", 232 + "in": "query", 233 + "schema": { 234 + "type": "integer" 235 + } 236 + }, 237 + { 238 + "name": "sort", 239 + "in": "query", 240 + "description": "Sort order (default: links)", 241 + "schema": { 242 + "type": "string" 243 + } 244 + } 245 + ], 246 + "responses": { 247 + "200": { 248 + "description": "Statistics HTML Page", 183 249 "content": { 184 250 "text/html": { 185 251 "schema": {
+11
internal/data/mysql.go
··· 235 235 return err 236 236 } 237 237 238 + func (s *MySQLStore) GetRandomQuote(ctx context.Context) (*Quote, error) { 239 + query := `SELECT quoteID, timestamp, quote, author FROM quote ORDER BY RAND() LIMIT 1` 240 + row := s.db.QueryRowContext(ctx, query) 241 + 242 + var q Quote 243 + if err := row.Scan(&q.ID, &q.Timestamp, &q.Quote, &q.Author); err != nil { 244 + return nil, err 245 + } 246 + return &q, nil 247 + } 248 + 238 249 func (s *MySQLStore) GetUserStats(ctx context.Context, sortBy string, limit int, offset int) ([]UserStat, error) { 239 250 // Sort logic 240 251 orderBy := "link_count DESC"
+11
internal/data/sqlite.go
··· 231 231 return err 232 232 } 233 233 234 + func (s *SQLiteStore) GetRandomQuote(ctx context.Context) (*Quote, error) { 235 + query := `SELECT quoteID, timestamp, quote, author FROM quote ORDER BY RANDOM() LIMIT 1` 236 + row := s.db.QueryRowContext(ctx, query) 237 + 238 + var q Quote 239 + if err := row.Scan(&q.ID, &q.Timestamp, &q.Quote, &q.Author); err != nil { 240 + return nil, err 241 + } 242 + return &q, nil 243 + } 244 + 234 245 func (s *SQLiteStore) GetUserStats(ctx context.Context, sortBy string, limit int, offset int) ([]UserStat, error) { 235 246 // Sort logic 236 247 orderBy := "link_count DESC"
+1
internal/data/store.go
··· 58 58 IncrementClicks(ctx context.Context, id int) error 59 59 InsertIRCLink(ctx context.Context, user, title, url, contentType string) (int, error) 60 60 InsertQuote(ctx context.Context, quote, author string) error 61 + GetRandomQuote(ctx context.Context) (*Quote, error) 61 62 62 63 // Stats 63 64 GetUserStats(ctx context.Context, sortBy string, limit int, offset int) ([]UserStat, error)
+35
internal/handler/quote.go
··· 1 1 package handler 2 2 3 3 import ( 4 + "encoding/json" 4 5 "fmt" 5 6 "html" 6 7 "net/http" 8 + "strings" 7 9 ) 8 10 9 11 // QuoteHandler handles /quote/ submissions ··· 13 15 quote := html.UnescapeString(r.FormValue("quote")) 14 16 author := html.UnescapeString(r.FormValue("author")) 15 17 18 + if quote == "" && author == "" { 19 + // No params -> Return a random quote (fortune style) 20 + q, err := h.Store.GetRandomQuote(ctx) 21 + if err != nil { 22 + http.Error(w, "Database Error", http.StatusInternalServerError) 23 + return 24 + } 25 + 26 + accept := r.Header.Get("Accept") 27 + if strings.Contains(accept, "application/json") { 28 + w.Header().Set("Content-Type", "application/json") 29 + if err := json.NewEncoder(w).Encode(q); err != nil { 30 + http.Error(w, "JSON Encoding Error", http.StatusInternalServerError) 31 + } 32 + return 33 + } 34 + 35 + responseText := fmt.Sprintf("%s -- %s", q.Quote, q.Author) 36 + 37 + if strings.Contains(accept, "text/html") { 38 + w.Header().Set("Content-Type", "text/html; charset=utf-8") 39 + // Escape for HTML safety 40 + fmt.Fprint(w, html.EscapeString(responseText)) 41 + return 42 + } 43 + 44 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") 45 + fmt.Fprint(w, responseText) 46 + return 47 + } 48 + 16 49 if quote != "" && author != "" { 50 + // Both params -> Insert Quote 17 51 // Perl code did uri_unescape. net/http request parsing handles standard form encoding. 18 52 // If these come in as query params or post body, FormValue gets them. 19 53 ··· 28 62 return 29 63 } 30 64 65 + // Partial params -> Error 31 66 http.Error(w, "Missing quote or author", http.StatusBadRequest) 32 67 }
+112
internal/handler/quote_test.go
··· 24 24 return nil 25 25 } 26 26 27 + func (m *MockStore) GetRandomQuote(ctx context.Context) (*data.Quote, error) { 28 + return &data.Quote{ 29 + Quote: "Random wisdom", 30 + Author: "Random Person", 31 + }, nil 32 + } 33 + 27 34 func TestQuoteHandler_UnescapesInput(t *testing.T) { 28 35 // Setup 29 36 mockStore := &MockStore{} ··· 61 68 t.Errorf("Expected author %q, got %q", expectedAuthor, mockStore.LastAuthor) 62 69 } 63 70 } 71 + 72 + func TestQuoteHandler_RandomQuote(t *testing.T) { 73 + // Setup 74 + mockStore := &MockStore{} 75 + h := &Handler{ 76 + Store: mockStore, 77 + Config: &config.Config{}, 78 + } 79 + 80 + // Test Case: No params -> Random Quote 81 + req := httptest.NewRequest("GET", "/quote/", nil) 82 + w := httptest.NewRecorder() 83 + 84 + // Execute 85 + h.QuoteHandler(w, req) 86 + 87 + // Verify 88 + if w.Code != http.StatusOK { 89 + t.Errorf("Expected status 200, got %d", w.Code) 90 + } 91 + 92 + expectedBody := "Random wisdom -- Random Person" 93 + if w.Body.String() != expectedBody { 94 + t.Errorf("Expected body %q, got %q", expectedBody, w.Body.String()) 95 + } 96 + } 97 + 98 + func TestQuoteHandler_RandomQuote_ContentNegotiation(t *testing.T) { 99 + // Setup 100 + mockStore := &MockStore{} 101 + h := &Handler{ 102 + Store: mockStore, 103 + Config: &config.Config{}, 104 + } 105 + 106 + // Test Case: JSON 107 + reqJSON := httptest.NewRequest("GET", "/quote/", nil) 108 + reqJSON.Header.Set("Accept", "application/json") 109 + wJSON := httptest.NewRecorder() 110 + h.QuoteHandler(wJSON, reqJSON) 111 + 112 + if wJSON.Code != http.StatusOK { 113 + t.Errorf("JSON: Expected status 200, got %d", wJSON.Code) 114 + } 115 + if contentType := wJSON.Header().Get("Content-Type"); contentType != "application/json" { 116 + t.Errorf("JSON: Expected Content-Type application/json, got %q", contentType) 117 + } 118 + // Basic JSON check 119 + if !strings.Contains(wJSON.Body.String(), `"quote":"Random wisdom"`) { 120 + t.Errorf("JSON: Expected quote body, got %q", wJSON.Body.String()) 121 + } 122 + 123 + // Test Case: HTML 124 + reqHTML := httptest.NewRequest("GET", "/quote/", nil) 125 + reqHTML.Header.Set("Accept", "text/html") 126 + wHTML := httptest.NewRecorder() 127 + h.QuoteHandler(wHTML, reqHTML) 128 + 129 + if wHTML.Code != http.StatusOK { 130 + t.Errorf("HTML: Expected status 200, got %d", wHTML.Code) 131 + } 132 + if contentType := wHTML.Header().Get("Content-Type"); !strings.Contains(contentType, "text/html") { 133 + t.Errorf("HTML: Expected Content-Type text/html, got %q", contentType) 134 + } 135 + // HTML body should be escaped if needed, but "Random wisdom" is safe. 136 + // Let's verify string presence. 137 + if !strings.Contains(wHTML.Body.String(), "Random wisdom -- Random Person") { 138 + t.Errorf("HTML: Expected quote body, got %q", wHTML.Body.String()) 139 + } 140 + } 141 + 142 + func TestQuoteHandler_PartialParams(t *testing.T) { 143 + // Setup 144 + mockStore := &MockStore{} 145 + h := &Handler{ 146 + Store: mockStore, 147 + Config: &config.Config{}, 148 + } 149 + 150 + // Test Case: Only quote provided 151 + form := url.Values{} 152 + form.Add("quote", "Only quote") 153 + req := httptest.NewRequest("POST", "/quote/", strings.NewReader(form.Encode())) 154 + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 155 + w := httptest.NewRecorder() 156 + 157 + h.QuoteHandler(w, req) 158 + 159 + if w.Code != http.StatusBadRequest { 160 + t.Errorf("Expected status 400 for partial params, got %d", w.Code) 161 + } 162 + 163 + // Test Case: Only author provided 164 + form = url.Values{} 165 + form.Add("author", "Only author") 166 + req = httptest.NewRequest("POST", "/quote/", strings.NewReader(form.Encode())) 167 + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 168 + w = httptest.NewRecorder() 169 + 170 + h.QuoteHandler(w, req) 171 + 172 + if w.Code != http.StatusBadRequest { 173 + t.Errorf("Expected status 400 for partial params, got %d", w.Code) 174 + } 175 + }