diff options
| -rw-r--r-- | android/app/src/main/AndroidManifest.xml | 7 | ||||
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt | 20 | ||||
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt | 37 | ||||
| -rw-r--r-- | android/app/src/main/java/org/terst/doot/widget/ui/EventDetailActivity.kt | 145 | ||||
| -rw-r--r-- | cmd/dashboard/main.go | 1 | ||||
| -rw-r--r-- | internal/api/google_calendar.go | 98 | ||||
| -rw-r--r-- | internal/api/google_calendar_test.go | 30 | ||||
| -rw-r--r-- | internal/api/interfaces.go | 1 | ||||
| -rw-r--r-- | internal/handlers/timeline_logic_test.go | 7 | ||||
| -rw-r--r-- | internal/handlers/widget.go | 22 | ||||
| -rw-r--r-- | internal/handlers/widget_test.go | 47 |
11 files changed, 10 insertions, 405 deletions
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 74d19ea..ca96e71 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -33,13 +33,6 @@ android:exported="false" android:windowSoftInputMode="adjustResize" /> - <!-- Event detail bottom sheet (opened by widget calendar event taps) --> - <activity - android:name=".ui.EventDetailActivity" - android:theme="@style/Theme.TaskDetail" - android:taskAffinity="${applicationId}.eventdetail" - android:exported="false" /> - <!-- Settings activity (also the widget config screen) --> <activity android:name=".ui.SettingsActivity" diff --git a/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt b/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt index 3574eb5..134e3bc 100644 --- a/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt +++ b/android/app/src/main/java/org/terst/doot/widget/data/WidgetRepository.kt @@ -19,9 +19,6 @@ private data class UpdateDescriptionRequest(val id: String, val source: String, @Serializable private data class WidgetAddRequest(val title: String) -@Serializable -private data class RecurrenceResponse(val recurrence: String) - /** * Optimistically drops an item from the cached list so the widget can update instantly on * completion, ahead of the authoritative refresh a worker performs once the API call succeeds. @@ -148,21 +145,4 @@ class WidgetRepository( check(response.isSuccessful) { "HTTP ${response.code}" } } } - - /** GETs the formatted recurrence schedule for a recurring event. */ - suspend fun getRecurrence(recurringEventId: String): Result<String> = - withContext(Dispatchers.IO) { - val encodedId = java.net.URLEncoder.encode(recurringEventId, "UTF-8") - val request = Request.Builder() - .url("$serverUrl/api/widget/recurrence?recurring_event_id=$encodedId") - .header("Authorization", "Bearer $token") - .build() - runCatching { - val response = client.newCall(request).execute() - check(response.isSuccessful) { "HTTP ${response.code}" } - val body = checkNotNull(response.body?.string()) { "Empty body" } - val parsed = json.decodeFromString<RecurrenceResponse>(body) - parsed.recurrence - } - } } diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt b/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt index 85a93d6..1983383 100644 --- a/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt +++ b/android/app/src/main/java/org/terst/doot/widget/ui/DootWidget.kt @@ -2,6 +2,7 @@ package org.terst.doot.widget.ui import android.content.Context import android.content.Intent +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -37,6 +38,12 @@ fun sourceColor(source: String): Color = when (source) { else -> Color(0xFF888888) } +/** Opens the event's source URL (Google Calendar, Plan to Eat, etc.) directly. */ +fun calendarViewIntent(url: String): Intent = + Intent(Intent.ACTION_VIEW, Uri.parse(url.ifEmpty { "https://calendar.google.com" })).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + private val json = Json { ignoreUnknownKeys = true } class DootWidget : GlanceAppWidget() { @@ -154,20 +161,12 @@ fun WidgetRoot(items: List<WidgetItem>, now: Instant, isRefreshing: Boolean, tex @Composable fun AllDayRow(event: WidgetItem, textSize: WidgetTextSize) { - val context = LocalContext.current - val detailIntent = Intent(context, EventDetailActivity::class.java).apply { - putExtra(EventDetailActivity.EXTRA_TITLE, event.title) - putExtra(EventDetailActivity.EXTRA_SOURCE, event.source) - putExtra(EventDetailActivity.EXTRA_URL, event.url) - putExtra(EventDetailActivity.EXTRA_RECURRING_EVENT_ID, event.recurringEventId) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } val color = sourceColor(event.source) Row( modifier = GlanceModifier .fillMaxWidth() .padding(vertical = 3.dp) - .clickable(actionStartActivity(detailIntent)), + .clickable(actionStartActivity(calendarViewIntent(event.url))), verticalAlignment = Alignment.CenterVertically ) { Box(modifier = GlanceModifier.width(3.dp).height(16.dp).background(color)) {} @@ -291,21 +290,13 @@ fun HourRow( @Composable fun EventBlock(event: WidgetItem, isPast: Boolean, textSize: WidgetTextSize) { - val context = LocalContext.current - val detailIntent = Intent(context, EventDetailActivity::class.java).apply { - putExtra(EventDetailActivity.EXTRA_TITLE, event.title) - putExtra(EventDetailActivity.EXTRA_SOURCE, event.source) - putExtra(EventDetailActivity.EXTRA_URL, event.url) - putExtra(EventDetailActivity.EXTRA_RECURRING_EVENT_ID, event.recurringEventId) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } val color = sourceColor(event.source) val alpha = if (isPast) 0.5f else 1f Row( modifier = GlanceModifier .fillMaxWidth() .padding(vertical = 4.dp) - .clickable(actionStartActivity(detailIntent)), + .clickable(actionStartActivity(calendarViewIntent(event.url))), verticalAlignment = Alignment.CenterVertically ) { Box( @@ -453,14 +444,6 @@ fun TomorrowSection(items: List<WidgetItem>, fragments: List<TaskFragment>, allD @Composable fun TomorrowEventRow(event: WidgetItem, zone: ZoneId, textSize: WidgetTextSize) { - val context = LocalContext.current - val detailIntent = Intent(context, EventDetailActivity::class.java).apply { - putExtra(EventDetailActivity.EXTRA_TITLE, event.title) - putExtra(EventDetailActivity.EXTRA_SOURCE, event.source) - putExtra(EventDetailActivity.EXTRA_URL, event.url) - putExtra(EventDetailActivity.EXTRA_RECURRING_EVENT_ID, event.recurringEventId) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } val color = sourceColor(event.source) val timeLabel = event.start?.let { val t = Instant.parse(it).atZone(zone) @@ -470,7 +453,7 @@ fun TomorrowEventRow(event: WidgetItem, zone: ZoneId, textSize: WidgetTextSize) modifier = GlanceModifier .fillMaxWidth() .padding(vertical = 3.dp) - .clickable(actionStartActivity(detailIntent)), + .clickable(actionStartActivity(calendarViewIntent(event.url))), verticalAlignment = Alignment.CenterVertically ) { Text( diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/EventDetailActivity.kt b/android/app/src/main/java/org/terst/doot/widget/ui/EventDetailActivity.kt deleted file mode 100644 index b8423ce..0000000 --- a/android/app/src/main/java/org/terst/doot/widget/ui/EventDetailActivity.kt +++ /dev/null @@ -1,145 +0,0 @@ -package org.terst.doot.widget.ui - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import okhttp3.OkHttpClient -import org.terst.doot.widget.data.Keys -import org.terst.doot.widget.data.WidgetRepository -import org.terst.doot.widget.data.dataStore - -class EventDetailActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val title = intent.getStringExtra(EXTRA_TITLE) ?: "" - val source = intent.getStringExtra(EXTRA_SOURCE) ?: "calendar" - val url = intent.getStringExtra(EXTRA_URL) ?: "" - val recurringEventId = intent.getStringExtra(EXTRA_RECURRING_EVENT_ID) - - setContent { - MaterialTheme(colorScheme = darkColorScheme()) { - EventDetailSheet( - title = title, - source = source, - recurringEventId = recurringEventId, - onOpenCalendar = { - startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse(url.ifEmpty { "https://calendar.google.com" })) - ) - finish() - }, - onDismiss = ::finish - ) - } - } - } - - companion object { - const val EXTRA_TITLE = "event_title" - const val EXTRA_SOURCE = "event_source" - const val EXTRA_URL = "event_url" - const val EXTRA_RECURRING_EVENT_ID = "event_recurring_id" - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun EventDetailSheet( - title: String, - source: String, - recurringEventId: String?, - onOpenCalendar: () -> Unit, - onDismiss: () -> Unit -) { - val context = LocalContext.current - var recurrenceText by remember { mutableStateOf<String?>(null) } - - LaunchedEffect(recurringEventId) { - if (recurringEventId == null) return@LaunchedEffect - recurrenceText = "Loading…" - val prefs = context.dataStore.data.first() - val url = prefs[Keys.SERVER_URL]?.trimEnd('/') ?: return@LaunchedEffect - val token = prefs[Keys.TOKEN] ?: return@LaunchedEffect - val repo = WidgetRepository(OkHttpClient(), url, token) - repo.getRecurrence(recurringEventId).onSuccess { text -> - recurrenceText = text - }.onFailure { - recurrenceText = null - } - } - - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), - containerColor = Color(0xFF1E293B), - tonalElevation = 0.dp, - dragHandle = { - Box( - modifier = Modifier - .padding(vertical = 12.dp) - .width(36.dp) - .height(4.dp) - .background(Color.White.copy(alpha = 0.25f), RoundedCornerShape(2.dp)) - ) - } - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .navigationBarsPadding() - ) { - Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(10.dp) - .background(sourceColor(source), RoundedCornerShape(5.dp)) - ) - Spacer(Modifier.width(10.dp)) - Text( - text = title, - fontSize = 17.sp, - fontWeight = FontWeight.SemiBold, - color = Color.White, - modifier = Modifier.weight(1f) - ) - } - recurrenceText?.let { text -> - Spacer(Modifier.height(8.dp)) - Text( - text = text, - fontSize = 13.sp, - color = Color.White.copy(alpha = 0.6f) - ) - } - Spacer(Modifier.height(20.dp)) - OutlinedButton( - onClick = onOpenCalendar, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White), - border = androidx.compose.foundation.BorderStroke(1.dp, Color.White.copy(alpha = 0.3f)) - ) { - Text("Open in Calendar", fontSize = 15.sp) - } - Spacer(Modifier.height(20.dp)) - } - } -} diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index d04a60e..eced067 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -373,7 +373,6 @@ func main() { r.With(widgetAuth).Post("/api/widget/update", h.HandleWidgetUpdate) r.With(widgetAuth).Post("/api/widget/complete", h.HandleWidgetComplete) r.With(widgetAuth).Post("/api/widget/reschedule", h.HandleWidgetReschedule) - r.With(widgetAuth).Get("/api/widget/recurrence", h.HandleWidgetRecurrence) r.With(widgetAuth).Post("/api/widget/add", h.HandleWidgetAdd) } else { log.Println("WIDGET_TOKEN not set — /api/widget disabled") diff --git a/internal/api/google_calendar.go b/internal/api/google_calendar.go index bb17812..f441a45 100644 --- a/internal/api/google_calendar.go +++ b/internal/api/google_calendar.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "sort" - "strconv" "strings" "time" @@ -84,86 +83,6 @@ func deduplicateEvents(events []models.CalendarEvent) []models.CalendarEvent { return unique } -var recurrenceFreqNames = map[string]string{ - "DAILY": "daily", - "WEEKLY": "weekly", - "MONTHLY": "monthly", - "YEARLY": "yearly", -} - -var recurrenceFreqUnits = map[string]string{ - "DAILY": "day", - "WEEKLY": "week", - "MONTHLY": "month", - "YEARLY": "year", -} - -var recurrenceWeekdayNames = map[string]string{ - "SU": "Sunday", - "MO": "Monday", - "TU": "Tuesday", - "WE": "Wednesday", - "TH": "Thursday", - "FR": "Friday", - "SA": "Saturday", -} - -// formatRecurrence turns Google Calendar RRULE strings into short English. -// This intentionally covers only the common cases (FREQ, INTERVAL, BYDAY) -- -// not a full RFC 5545 parser. Anything it can't confidently describe falls -// back to "Recurring event" rather than showing nothing or an error. -func formatRecurrence(rrules []string) string { - for _, rule := range rrules { - rule = strings.TrimPrefix(rule, "RRULE:") - parts := make(map[string]string) - for _, kv := range strings.Split(rule, ";") { - pieces := strings.SplitN(kv, "=", 2) - if len(pieces) == 2 { - parts[pieces[0]] = pieces[1] - } - } - - freqKey := parts["FREQ"] - unit, ok := recurrenceFreqUnits[freqKey] - if !ok { - continue - } - - interval := 1 - if iv := parts["INTERVAL"]; iv != "" { - if n, err := strconv.Atoi(iv); err == nil && n > 0 { - interval = n - } - } - - var phrase string - if interval == 1 { - phrase = "Repeats " + recurrenceFreqNames[freqKey] - } else { - phrase = fmt.Sprintf("Repeats every %d %ss", interval, unit) - } - - if byday := parts["BYDAY"]; byday != "" { - var days []string - for _, code := range strings.Split(byday, ",") { - code = strings.TrimSpace(code) - if len(code) >= 2 { - code = code[len(code)-2:] - } - if name, ok := recurrenceWeekdayNames[code]; ok { - days = append(days, name) - } - } - if len(days) > 0 { - phrase += " on " + strings.Join(days, ", ") - } - } - - return phrase - } - return "Recurring event" -} - // NewGoogleCalendarClient creates a client that fetches from multiple calendars. // calendarIDs can be comma-separated (e.g., "cal1@group.calendar.google.com,cal2@group.calendar.google.com") // timezone is the IANA timezone name for display (e.g., "Pacific/Honolulu") @@ -285,20 +204,3 @@ func (c *GoogleCalendarClient) GetCalendarList(ctx context.Context) ([]models.Ca return calendars, nil } -// GetRecurrenceRule looks up a recurring event's master record and returns -// its formatted recurrence schedule. Google's API only puts the RRULE on -// the master event, not on expanded instances (see parseEventTime's -// SingleEvents(true) callers), so this does a live lookup by the instance's -// RecurringEventId. There's no per-event calendar attribution stored today -// (events from all configured calendars are merged without recording which -// one they came from), so this tries each configured calendar in turn. -func (c *GoogleCalendarClient) GetRecurrenceRule(ctx context.Context, recurringEventID string) (string, error) { - for _, calendarID := range c.calendarIDs { - event, err := c.srv.Events.Get(calendarID, recurringEventID).Do() - if err != nil { - continue - } - return formatRecurrence(event.Recurrence), nil - } - return "", fmt.Errorf("recurring event %s not found on any configured calendar", recurringEventID) -} diff --git a/internal/api/google_calendar_test.go b/internal/api/google_calendar_test.go index efbab0d..3cf0dbd 100644 --- a/internal/api/google_calendar_test.go +++ b/internal/api/google_calendar_test.go @@ -272,33 +272,3 @@ func TestGetUpcomingEvents_APIError_ReturnsEmptyNotError(t *testing.T) { t.Errorf("expected 0 events on API error, got %d", len(events)) } } - -// --- formatRecurrence --- - -func TestFormatRecurrence(t *testing.T) { - tests := []struct { - name string - rules []string - want string - }{ - {"weekly single day", []string{"RRULE:FREQ=WEEKLY;BYDAY=MO"}, "Repeats weekly on Monday"}, - {"weekly multiple days", []string{"RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR"}, "Repeats weekly on Monday, Wednesday, Friday"}, - {"daily", []string{"RRULE:FREQ=DAILY"}, "Repeats daily"}, - {"monthly", []string{"RRULE:FREQ=MONTHLY"}, "Repeats monthly"}, - {"yearly", []string{"RRULE:FREQ=YEARLY"}, "Repeats yearly"}, - {"interval weekly", []string{"RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU"}, "Repeats every 2 weeks on Sunday"}, - {"interval daily", []string{"RRULE:FREQ=DAILY;INTERVAL=3"}, "Repeats every 3 days"}, - {"unrecognized frequency", []string{"RRULE:FREQ=HOURLY"}, "Recurring event"}, - {"empty", []string{}, "Recurring event"}, - {"unparseable", []string{"not a valid rule"}, "Recurring event"}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := formatRecurrence(tc.rules) - if got != tc.want { - t.Errorf("formatRecurrence(%v) = %q, want %q", tc.rules, got, tc.want) - } - }) - } -} diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go index 3c1e4e1..183f3f0 100644 --- a/internal/api/interfaces.go +++ b/internal/api/interfaces.go @@ -30,7 +30,6 @@ type GoogleCalendarAPI interface { GetEventsByDateRange(ctx context.Context, start, end time.Time) ([]models.CalendarEvent, error) GetCalendarList(ctx context.Context) ([]models.CalendarInfo, error) SetCalendarIDs(ids []string) - GetRecurrenceRule(ctx context.Context, recurringEventID string) (string, error) } // GoogleTasksAPI defines the interface for Google Tasks operations diff --git a/internal/handlers/timeline_logic_test.go b/internal/handlers/timeline_logic_test.go index 1da2d8a..01ad814 100644 --- a/internal/handlers/timeline_logic_test.go +++ b/internal/handlers/timeline_logic_test.go @@ -23,9 +23,6 @@ type MockCalendarClient struct { // with, for tests that need to assert on how the caller resolved its // calendar ID list (e.g. fetchCalendarEvents' comma-split fallback). SetCalendarIDsCalls [][]string - // RecurrenceRule is returned by GetRecurrenceRule for any id when RecurrenceErr is nil. - RecurrenceRule string - RecurrenceErr error } func (m *MockCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error) { @@ -44,10 +41,6 @@ func (m *MockCalendarClient) SetCalendarIDs(ids []string) { m.SetCalendarIDsCalls = append(m.SetCalendarIDsCalls, ids) } -func (m *MockCalendarClient) GetRecurrenceRule(ctx context.Context, recurringEventID string) (string, error) { - return m.RecurrenceRule, m.RecurrenceErr -} - func setupTestStore(t *testing.T) *store.Store { t.Helper() tempDir := t.TempDir() diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index f1f7452..0092b6b 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -359,28 +359,6 @@ func (h *Handler) HandleWidgetComplete(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -// HandleWidgetRecurrence looks up and formats a recurring calendar event's schedule. -func (h *Handler) HandleWidgetRecurrence(w http.ResponseWriter, r *http.Request) { - recurringEventID := r.URL.Query().Get("recurring_event_id") - if recurringEventID == "" { - http.Error(w, "recurring_event_id is required", http.StatusBadRequest) - return - } - - recurrence, err := h.googleCalendarClient.GetRecurrenceRule(r.Context(), recurringEventID) - if err != nil { - http.Error(w, "recurring event not found", http.StatusNotFound) - return - } - - resp := struct { - Recurrence string `json:"recurrence"` - }{Recurrence: recurrence} - - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(resp) -} - // HandleWidgetAdd creates a new undated native task from the widget's quick-add sheet. func (h *Handler) HandleWidgetAdd(w http.ResponseWriter, r *http.Request) { var req widgetAddRequest diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go index 3116918..4e08d2a 100644 --- a/internal/handlers/widget_test.go +++ b/internal/handlers/widget_test.go @@ -3,7 +3,6 @@ package handlers import ( "context" "encoding/json" - "fmt" "net/http" "net/http/httptest" "strings" @@ -788,49 +787,3 @@ func TestTimelineItemToWidgetItem_NonRecurring_EmptyRecurringEventID(t *testing. } } -// TestHandleWidgetRecurrence_ReturnsFormattedSchedule proves the recurrence -// lookup endpoint: given a recurring_event_id query param, it calls the -// calendar client's GetRecurrenceRule and returns the formatted text. -func TestHandleWidgetRecurrence_ReturnsFormattedSchedule(t *testing.T) { - mock := &MockCalendarClient{RecurrenceRule: "Repeats weekly on Monday"} - h := &Handler{googleCalendarClient: mock} - req := httptest.NewRequest("GET", "/api/widget/recurrence?recurring_event_id=master-1", nil) - w := httptest.NewRecorder() - http.HandlerFunc(h.HandleWidgetRecurrence).ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d", w.Code) - } - var resp struct { - Recurrence string `json:"recurrence"` - } - if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { - t.Fatalf("failed to decode response: %v", err) - } - if resp.Recurrence != "Repeats weekly on Monday" { - t.Errorf("recurrence = %q, want %q", resp.Recurrence, "Repeats weekly on Monday") - } -} - -func TestHandleWidgetRecurrence_NotFound_Returns404(t *testing.T) { - mock := &MockCalendarClient{RecurrenceErr: fmt.Errorf("not found")} - h := &Handler{googleCalendarClient: mock} - req := httptest.NewRequest("GET", "/api/widget/recurrence?recurring_event_id=missing", nil) - w := httptest.NewRecorder() - http.HandlerFunc(h.HandleWidgetRecurrence).ServeHTTP(w, req) - - if w.Code != http.StatusNotFound { - t.Fatalf("expected 404, got %d", w.Code) - } -} - -func TestHandleWidgetRecurrence_MissingParam_Returns400(t *testing.T) { - h := &Handler{} - req := httptest.NewRequest("GET", "/api/widget/recurrence", nil) - w := httptest.NewRecorder() - http.HandlerFunc(h.HandleWidgetRecurrence).ServeHTTP(w, req) - - if w.Code != http.StatusBadRequest { - t.Fatalf("expected 400, got %d", w.Code) - } -} |
