Files
Crussell/backend/handlers/bookings/admin_reserve_test.go
T
popertotsandSisyphus 7698ca636b feat(bookings): support out_of_hours flag in admin reserve handler
Add OutOfHours field to AdminReserveSlotRequest. When true, skip the closing hours check allowing admin to reserve slots outside normal business hours. Add tests for call-in, walk-in, without-flag failure, and time blocker interaction.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-22 12:54:52 +01:00

705 lines
22 KiB
Go

//go:build test && dev
// +build test,dev
package bookings
// Package bookings contains tests for admin booking reservation endpoints.
//
// Test Coverage:
// - AdminReserveSlotHandler: POST /api/admin/bookings/reserve - Admin slot reservation (walk-in or call-in)
//
// Tests cover walk-in and call-in reservation types, validation, slot overlaps, and replacement behavior.
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"crussell/testutils"
"crussell/mw"
"crussell/testutils/fixtures"
"github.com/go-chi/chi/v5"
)
// makeAdminReserveRequest creates a request with admin context for admin reserve slot handler
// It sets mw.UserIDKey and mw.UserRoleKey to "admin" in the context
func makeAdminReserveRequest(handler http.Handler, body interface{}, adminID string, requestCtx ...context.Context) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
bodyBytes, _ := json.Marshal(body)
req = httptest.NewRequest("POST", "/api/admin/bookings/reserve", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest("POST", "/api/admin/bookings/reserve", nil)
}
baseCtx := req.Context()
if len(requestCtx) > 0 {
baseCtx = requestCtx[0]
}
rctx := chi.NewRouteContext()
ctx := context.WithValue(baseCtx, chi.RouteCtxKey, rctx)
ctx = context.WithValue(ctx, mw.UserIDKey, adminID)
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
return w
}
// =============================================================================
// Walk-in Reservation Tests
// =============================================================================
// TestAdminReserveSlot_WalkIn_Success tests that an admin can successfully
// create a walk-in reservation with a valid duration. The test verifies
// the reservation is created in the database with the correct duration.
func TestAdminReserveSlot_WalkIn_Success(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
tomorrow := time.Now().Add(24 * time.Hour)
now := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 12, 0, 0, 0, tomorrow.Location())
req := AdminReserveSlotRequest{
ReservationType: "walkin",
StartTime: now,
DurationMinutes: 30,
TTLMinutes: 15,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
// Parse response and verify duration
var resp AdminReserveSlotResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp.DurationMinutes != 30 {
t.Errorf("expected duration 30, got %d", resp.DurationMinutes)
}
// Verify time_blocker was created with correct description pattern
var desc string
err = tx.QueryRow(ctx,
"SELECT description FROM time_blockers WHERE description LIKE 'RESERVATION:admin:walkin:%'",
).Scan(&desc)
if err != nil {
t.Errorf("failed to query time_blocker: %v", err)
}
if !strings.HasPrefix(desc, "RESERVATION:admin:walkin:") {
t.Errorf("expected description to start with 'RESERVATION:admin:walkin:', got %s", desc)
}
}
// =============================================================================
// Call-in Reservation Tests
// =============================================================================
// TestAdminReserveSlot_CallIn_Success tests that an admin can successfully
// create a call-in reservation with valid service IDs. The test verifies
// the reservation duration matches the service duration.
func TestAdminReserveSlot_CallIn_Success(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(tx, userID)
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(tx, serviceID)
_, err = tx.Exec(ctx, "UPDATE services SET duration_minutes = 30 WHERE id = $1", serviceID)
if err != nil {
t.Fatalf("failed to update service duration: %v", err)
}
tomorrow := time.Now().Add(24 * time.Hour).Truncate(time.Second)
tomorrow = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 10, 0, 0, 0, tomorrow.Location())
req := AdminReserveSlotRequest{
UserID: &userID,
ReservationType: "callin",
StartTime: tomorrow,
ServiceIDs: []string{serviceID},
TTLMinutes: 15,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
// Parse response and verify duration matches service (30 min)
var resp AdminReserveSlotResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp.DurationMinutes != 30 {
t.Errorf("expected duration 30, got %d", resp.DurationMinutes)
}
// Verify time_blocker was created with correct description pattern
var desc string
err = tx.QueryRow(ctx,
"SELECT description FROM time_blockers WHERE description LIKE 'RESERVATION:admin:callin:%'",
).Scan(&desc)
if err != nil {
t.Errorf("failed to query time_blocker: %v", err)
}
if !strings.HasPrefix(desc, "RESERVATION:admin:callin:") {
t.Errorf("expected description to start with 'RESERVATION:admin:callin:', got %s", desc)
}
}
// =============================================================================
// Validation Tests - Missing Duration
// =============================================================================
// TestAdminReserveSlot_WalkIn_MissingDuration tests that walk-in reservations
// fail with HTTP 400 when duration_minutes is missing or zero.
func TestAdminReserveSlot_WalkIn_MissingDuration(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
now := time.Now()
req := AdminReserveSlotRequest{
ReservationType: "walkin",
StartTime: now,
TTLMinutes: 15,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
// Verify error message mentions duration_minutes
body := w.Body.String()
if !strings.Contains(body, "duration_minutes") {
t.Errorf("expected body to contain 'duration_minutes', got %s", body)
}
}
// =============================================================================
// Validation Tests - Missing Services
// =============================================================================
// TestAdminReserveSlot_CallIn_MissingServices tests that call-in reservations
// fail with HTTP 400 when service_ids is empty.
func TestAdminReserveSlot_CallIn_MissingServices(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
tomorrow := time.Now().Add(24 * time.Hour).Truncate(time.Second)
tomorrow = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 10, 0, 0, 0, tomorrow.Location())
req := AdminReserveSlotRequest{
ReservationType: "callin",
StartTime: tomorrow,
ServiceIDs: []string{},
TTLMinutes: 15,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
// Verify error message mentions service
body := w.Body.String()
if !strings.Contains(body, "service") {
t.Errorf("expected body to contain 'service', got %s", body)
}
}
// =============================================================================
// Validation Tests - Invalid Reservation Type
// =============================================================================
// TestAdminReserveSlot_InvalidReservationType tests that reservations
// fail with HTTP 400 when reservation_type is invalid.
func TestAdminReserveSlot_InvalidReservationType(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
req := AdminReserveSlotRequest{
ReservationType: "invalid",
StartTime: time.Now(),
DurationMinutes: 30,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
// Verify error message mentions valid types
body := w.Body.String()
if !strings.Contains(body, "walkin") && !strings.Contains(body, "callin") {
t.Errorf("expected body to contain 'walkin' or 'callin', got %s", body)
}
}
// =============================================================================
// Slot Overlap Tests
// =============================================================================
// TestAdminReserveSlot_SlotOverlap tests that a reservation fails
// with HTTP 409 when the slot overlaps with an existing booking.
func TestAdminReserveSlot_SlotOverlap(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
// Create test admin user (for the booking)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
// Create test regular user
userID, err := fixtures.CreateTestUser(tx)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(tx, userID)
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create test service: %v", err)
}
defer fixtures.DeleteService(tx, serviceID)
// Set deposits_required=0 for test user
_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
if err != nil {
t.Fatalf("failed to set deposits_required: %v", err)
}
tomorrow := time.Now().Add(24 * time.Hour).Truncate(time.Second)
tomorrow = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 10, 0, 0, 0, tomorrow.Location())
bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
if err != nil {
t.Fatalf("failed to create test booking: %v", err)
}
defer fixtures.DeleteBooking(tx, bookingID)
_, err = tx.Exec(ctx,
"UPDATE bookings SET start_time = $1, status = 'confirmed' WHERE id = $2",
tomorrow, bookingID)
if err != nil {
t.Fatalf("failed to update booking time: %v", err)
}
// Now try to reserve a call-in that overlaps (tomorrow 10:15 - 15 min after start)
overlapTime := tomorrow.Add(15 * time.Minute)
req := AdminReserveSlotRequest{
ReservationType: "callin",
StartTime: overlapTime,
ServiceIDs: []string{serviceID},
TTLMinutes: 15,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
// Should return 409 Conflict due to overlap
if w.Code != http.StatusConflict {
t.Errorf("expected status 409, got %d. body: %s", w.Code, w.Body.String())
}
}
// =============================================================================
// Reservation Replacement Tests
// =============================================================================
// TestAdminReserveSlot_ReplacesExisting tests that reserving twice
// on the same admin replaces the previous reservation.
func TestAdminReserveSlot_ReplacesExisting(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
tomorrow := time.Now().Add(24 * time.Hour)
now := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 12, 0, 0, 0, tomorrow.Location())
req := AdminReserveSlotRequest{
ReservationType: "walkin",
StartTime: now,
DurationMinutes: 30,
TTLMinutes: 15,
}
// First reservation
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
var firstResp AdminReserveSlotResponse
if err := json.Unmarshal(w.Body.Bytes(), &firstResp); err != nil {
t.Fatalf("failed to parse first response: %v", err)
}
// Count reservations before second request
var countBefore int
var qerr error
qerr = tx.QueryRow(ctx,
"SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:admin:%'",
).Scan(&countBefore)
if qerr != nil {
t.Fatalf("failed to count reservations: %v", qerr)
}
// Second reservation (same admin, new time)
laterTime := now.Add(1 * time.Hour)
req2 := AdminReserveSlotRequest{
ReservationType: "walkin",
StartTime: laterTime,
DurationMinutes: 45,
TTLMinutes: 15,
}
w = makeAdminReserveRequest(handler, req2, adminID, ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
var secondResp AdminReserveSlotResponse
if err := json.Unmarshal(w.Body.Bytes(), &secondResp); err != nil {
t.Fatalf("failed to parse second response: %v", err)
}
// Count reservations after second request - should still be 1
var countAfter int
qerr = tx.QueryRow(ctx,
"SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:admin:%'",
).Scan(&countAfter)
if qerr != nil {
t.Fatalf("failed to count reservations: %v", qerr)
}
// Old one should be deleted, new one exists (id should be different)
if countAfter != 1 {
t.Errorf("expected 1 reservation after replacement, got %d", countAfter)
}
if firstResp.ID == secondResp.ID {
t.Errorf("expected new reservation ID to be different from old one")
}
}
// =============================================================================
// Past Start Time Tests
// =============================================================================
// TestAdminReserveSlot_WalkIn_PastStart tests that walk-in reservations
func TestAdminReserveSlot_WalkIn_PastStart(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
pastTime := time.Now().Add(-5 * time.Minute)
req := AdminReserveSlotRequest{
ReservationType: "walkin",
StartTime: pastTime,
DurationMinutes: 30,
TTLMinutes: 15,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
// Verify error message mentions the past time limit
body := w.Body.String()
if !strings.Contains(body, "past") {
t.Errorf("expected body to contain 'past', got %s", body)
}
}
// TestAdminReserveSlot_OutOfHours_CallIn_Success verifies that an admin can
// successfully create a call-in reservation with out_of_hours=true, bypassing
// the closing hours check even when booking outside normal hours.
func TestAdminReserveSlot_OutOfHours_CallIn_Success(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
defer fixtures.DeleteService(tx, serviceID)
// Baseline test DB has 08:00-20:00 hours. Book 19:30 + 60min = 20:30 (> 20:00 closing)
// Without out_of_hours this would fail; with out_of_hours=true it should succeed.
tomorrow := time.Now().Add(24 * time.Hour)
lateBooking := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 19, 30, 0, 0, tomorrow.Location())
req := AdminReserveSlotRequest{
ReservationType: "callin",
StartTime: lateBooking,
ServiceIDs: []string{serviceID},
TTLMinutes: 15,
OutOfHours: true,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201 for out-of-hours reservation, got %d. body: %s", w.Code, w.Body.String())
}
var resp AdminReserveSlotResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp.StartTime.Format("15:04") != "19:30" {
t.Errorf("expected start time 19:30, got %s", resp.StartTime.Format("15:04"))
}
}
// TestAdminReserveSlot_OutOfHours_WithoutFlag_Fails verifies that an admin
// attempting to book a slot that ends after closing hours WITHOUT the
// out_of_hours flag is rejected, ensuring the flag is required.
func TestAdminReserveSlot_OutOfHours_WithoutFlag_Fails(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
serviceID, err := fixtures.CreateTestService(tx)
if err != nil {
t.Fatalf("failed to create service: %v", err)
}
defer fixtures.DeleteService(tx, serviceID)
// Baseline test DB has 08:00-20:00 hours. Book 19:30 + 60min = 20:30 (> 20:00 closing)
// Without out_of_hours this should be rejected.
tomorrow := time.Now().Add(24 * time.Hour)
lateBooking := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 19, 30, 0, 0, tomorrow.Location())
req := AdminReserveSlotRequest{
ReservationType: "callin",
StartTime: lateBooking,
ServiceIDs: []string{serviceID},
TTLMinutes: 15,
// OutOfHours intentionally NOT set (defaults to false)
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 when booking beyond closing hours without out_of_hours flag, got %d. body: %s",
w.Code, w.Body.String())
}
}
// TestAdminReserveSlot_OutOfHours_WalkIn_Success verifies that an admin can
// successfully create a walk-in reservation with out_of_hours=true, bypassing
// the closing hours check and using explicit duration.
func TestAdminReserveSlot_OutOfHours_WalkIn_Success(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
// Baseline test DB has 08:00-20:00 hours. Book walk-in 19:30 + 60min = 20:30 (> 20:00 closing)
// Without out_of_hours this would fail; with out_of_hours=true it should succeed.
tomorrow := time.Now().Add(24 * time.Hour)
lateBooking := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 19, 30, 0, 0, tomorrow.Location())
req := AdminReserveSlotRequest{
ReservationType: "walkin",
StartTime: lateBooking,
DurationMinutes: 60,
TTLMinutes: 15,
OutOfHours: true,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201 for out-of-hours walk-in reservation, got %d. body: %s",
w.Code, w.Body.String())
}
var resp AdminReserveSlotResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp.DurationMinutes != 60 {
t.Errorf("expected duration 60, got %d", resp.DurationMinutes)
}
// Verify reservation time_blocker was created
var desc string
err = tx.QueryRow(ctx,
"SELECT description FROM time_blockers WHERE description LIKE 'RESERVATION:admin:walkin:%'",
).Scan(&desc)
if err != nil {
t.Errorf("failed to query time_blocker: %v", err)
}
if !strings.HasPrefix(desc, "RESERVATION:admin:walkin:") {
t.Errorf("expected RESERVATION:admin:walkin: prefix, got %s", desc)
}
}
// TestAdminReserveSlot_OutOfHours_TimeBlockerBlocks verifies that even when
// out_of_hours=true is set, a time blocker at the requested time still causes
// the reservation to be rejected with 409 Conflict.
func TestAdminReserveSlot_OutOfHours_TimeBlockerBlocks(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
defer fixtures.DeleteUser(tx, adminID)
// Create a time blocker at a specific future time
tomorrow := time.Now().Add(24 * time.Hour)
blockerStart := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 14, 0, 0, 0, tomorrow.Location())
_, err = tx.Exec(ctx, `
INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
VALUES ($1, 60, 'Admin Blocked Time', NULL)
`, blockerStart)
if err != nil {
t.Fatalf("failed to create time blocker: %v", err)
}
// Try reserving during the blocked time with out_of_hours=true
req := AdminReserveSlotRequest{
ReservationType: "walkin",
StartTime: blockerStart,
DurationMinutes: 30,
TTLMinutes: 15,
OutOfHours: true,
}
handler := http.HandlerFunc(AdminReserveSlotHandler)
w := makeAdminReserveRequest(handler, req, adminID, ctx)
if w.Code != http.StatusConflict {
t.Errorf("expected 409 Conflict when blocker overlaps with out_of_hours reservation, got %d. body: %s",
w.Code, w.Body.String())
}
}
// TestAdminReserveSlot_SlotOverlap tests that a reservation fails when the
// requested time slot overlaps with an existing booking.