mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:41:30 +00:00
7cb453a336
Mechanical reformat. The new 'gofmt drift' CI step (added in
ci-pipeline-cleanup Phase 4, commit 0f205a8) surfaced 111 files
with accumulated gofmt drift across cmd/, internal/, and deploy/test/.
Each file's diff is gofmt-standard: whitespace adjustments, intra-
group import sorting (alphabetical by import path within blank-line-
separated groups), and struct-tag column alignment. No semantic
changes — verified via 'git diff --ignore-all-space' which shows only
the line-position deltas from import reordering.
The gate stays in place after this commit. Going forward it catches
gofmt drift at PR time.
428 lines
11 KiB
Go
428 lines
11 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestEncodeCursor_ProducesValidBase64(t *testing.T) {
|
|
// Test that encodeCursor produces valid base64 with correct format
|
|
originalTime := time.Date(2024, 3, 15, 10, 30, 45, 123456789, time.UTC)
|
|
originalID := "cert-12345"
|
|
|
|
// Encode
|
|
encoded := encodeCursor(originalTime, originalID)
|
|
|
|
// Verify it's valid base64
|
|
decoded, err := base64.URLEncoding.DecodeString(encoded)
|
|
if err != nil {
|
|
t.Fatalf("encoded cursor is not valid base64: %v", err)
|
|
}
|
|
|
|
// Verify contains both timestamp and ID
|
|
decodedStr := string(decoded)
|
|
if !strings.Contains(decodedStr, originalID) {
|
|
t.Errorf("decoded cursor doesn't contain ID %q, got %q", originalID, decodedStr)
|
|
}
|
|
|
|
// Verify it's not empty and has expected structure (timestamp:id)
|
|
if !strings.Contains(decodedStr, ":") {
|
|
t.Errorf("decoded cursor doesn't contain colon separator, got %q", decodedStr)
|
|
}
|
|
}
|
|
|
|
func TestEncodeCursor_DifferentTimes(t *testing.T) {
|
|
id := "test-id"
|
|
time1 := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
time2 := time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)
|
|
|
|
cursor1 := encodeCursor(time1, id)
|
|
cursor2 := encodeCursor(time2, id)
|
|
|
|
// Different times should produce different cursors
|
|
if cursor1 == cursor2 {
|
|
t.Error("Different times produced identical cursors")
|
|
}
|
|
}
|
|
|
|
func TestEncodeCursor_DifferentIDs(t *testing.T) {
|
|
now := time.Now()
|
|
id1 := "cert-1"
|
|
id2 := "cert-2"
|
|
|
|
cursor1 := encodeCursor(now, id1)
|
|
cursor2 := encodeCursor(now, id2)
|
|
|
|
// Different IDs should produce different cursors
|
|
if cursor1 == cursor2 {
|
|
t.Error("Different IDs produced identical cursors")
|
|
}
|
|
}
|
|
|
|
func TestDecodeCursor_InvalidBase64(t *testing.T) {
|
|
// Create the decodeCursor function from the closure - matching actual behavior
|
|
decodeCursor := func(cursor string) (time.Time, string, error) {
|
|
raw, err := base64.URLEncoding.DecodeString(cursor)
|
|
if err != nil {
|
|
return time.Time{}, "", err
|
|
}
|
|
parts := strings.SplitN(string(raw), ":", 2)
|
|
if len(parts) != 2 {
|
|
return time.Time{}, "", fmt.Errorf("invalid cursor format")
|
|
}
|
|
t, err := time.Parse(time.RFC3339Nano, parts[0])
|
|
if err != nil {
|
|
return time.Time{}, "", err
|
|
}
|
|
return t, parts[1], nil
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
cursor string
|
|
expectError bool
|
|
}{
|
|
{"invalid base64", "!!!invalid!!!", true},
|
|
{"empty string", "", true},
|
|
{"no colon separator", base64.URLEncoding.EncodeToString([]byte("no-separator-here")), true},
|
|
{"invalid timestamp", base64.URLEncoding.EncodeToString([]byte("not-a-timestamp:id-123")), true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, _, err := decodeCursor(tt.cursor)
|
|
if tt.expectError && err == nil {
|
|
t.Error("expected error for invalid cursor, got nil")
|
|
}
|
|
if !tt.expectError && err != nil {
|
|
t.Errorf("expected no error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJSON_SetsContentType(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
data := map[string]string{"key": "value"}
|
|
|
|
JSON(w, http.StatusOK, data)
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", contentType)
|
|
}
|
|
}
|
|
|
|
func TestJSON_SetsStatusCode(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
data := map[string]string{"key": "value"}
|
|
|
|
JSON(w, http.StatusCreated, data)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("Status code = %d, want %d", w.Code, http.StatusCreated)
|
|
}
|
|
}
|
|
|
|
func TestJSON_EncodesData(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
data := map[string]interface{}{
|
|
"string": "value",
|
|
"number": 42,
|
|
"bool": true,
|
|
"null": nil,
|
|
}
|
|
|
|
JSON(w, http.StatusOK, data)
|
|
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if result["string"] != "value" {
|
|
t.Errorf("string = %v, want value", result["string"])
|
|
}
|
|
|
|
if result["number"] != float64(42) {
|
|
t.Errorf("number = %v, want 42", result["number"])
|
|
}
|
|
|
|
if result["bool"] != true {
|
|
t.Errorf("bool = %v, want true", result["bool"])
|
|
}
|
|
|
|
if result["null"] != nil {
|
|
t.Errorf("null = %v, want nil", result["null"])
|
|
}
|
|
}
|
|
|
|
func TestError_SetsStatusCode(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
Error(w, http.StatusBadRequest, "Invalid input")
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Status code = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func TestError_SetsContentType(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
Error(w, http.StatusBadRequest, "Invalid input")
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", contentType)
|
|
}
|
|
}
|
|
|
|
func TestError_IncludesMessage(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
message := "Something went wrong"
|
|
|
|
Error(w, http.StatusInternalServerError, message)
|
|
|
|
var errResp ErrorResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
|
|
t.Fatalf("failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp.Message != message {
|
|
t.Errorf("Message = %q, want %q", errResp.Message, message)
|
|
}
|
|
}
|
|
|
|
func TestError_IncludesStatusText(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
Error(w, http.StatusNotFound, "Resource not found")
|
|
|
|
var errResp ErrorResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
|
|
t.Fatalf("failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp.Error != http.StatusText(http.StatusNotFound) {
|
|
t.Errorf("Error = %q, want %q", errResp.Error, http.StatusText(http.StatusNotFound))
|
|
}
|
|
}
|
|
|
|
func TestErrorWithRequestID_SetsStatusCode(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
ErrorWithRequestID(w, http.StatusBadRequest, "Invalid input", "req-123")
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("Status code = %d, want %d", w.Code, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func TestErrorWithRequestID_IncludesRequestID(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
requestID := "req-abc-def-ghi"
|
|
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Server error", requestID)
|
|
|
|
var errResp ErrorResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
|
|
t.Fatalf("failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp.RequestID != requestID {
|
|
t.Errorf("RequestID = %q, want %q", errResp.RequestID, requestID)
|
|
}
|
|
}
|
|
|
|
func TestErrorWithRequestID_IncludesMessage(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
message := "Database connection failed"
|
|
|
|
ErrorWithRequestID(w, http.StatusServiceUnavailable, message, "req-123")
|
|
|
|
var errResp ErrorResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
|
|
t.Fatalf("failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp.Message != message {
|
|
t.Errorf("Message = %q, want %q", errResp.Message, message)
|
|
}
|
|
}
|
|
|
|
func TestPagedResponse_Structure(t *testing.T) {
|
|
response := PagedResponse{
|
|
Data: []string{"item1", "item2"},
|
|
Total: 100,
|
|
Page: 2,
|
|
PerPage: 50,
|
|
}
|
|
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal response: %v", err)
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if result["total"] != float64(100) {
|
|
t.Errorf("total = %v, want 100", result["total"])
|
|
}
|
|
|
|
if result["page"] != float64(2) {
|
|
t.Errorf("page = %v, want 2", result["page"])
|
|
}
|
|
|
|
if result["per_page"] != float64(50) {
|
|
t.Errorf("per_page = %v, want 50", result["per_page"])
|
|
}
|
|
|
|
if result["data"] == nil {
|
|
t.Error("data is nil")
|
|
}
|
|
}
|
|
|
|
func TestCursorPagedResponse_Structure(t *testing.T) {
|
|
response := CursorPagedResponse{
|
|
Data: []string{"item1", "item2"},
|
|
Total: 100,
|
|
NextCursor: "abc123def456",
|
|
PageSize: 50,
|
|
}
|
|
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal response: %v", err)
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if result["total"] != float64(100) {
|
|
t.Errorf("total = %v, want 100", result["total"])
|
|
}
|
|
|
|
if result["next_cursor"] != "abc123def456" {
|
|
t.Errorf("next_cursor = %v, want abc123def456", result["next_cursor"])
|
|
}
|
|
|
|
if result["page_size"] != float64(50) {
|
|
t.Errorf("page_size = %v, want 50", result["page_size"])
|
|
}
|
|
}
|
|
|
|
func TestCursorPagedResponse_EmptyNextCursor(t *testing.T) {
|
|
// When NextCursor is empty, it should be omitted from JSON
|
|
response := CursorPagedResponse{
|
|
Data: []string{},
|
|
Total: 0,
|
|
NextCursor: "",
|
|
PageSize: 50,
|
|
}
|
|
|
|
data, err := json.Marshal(response)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal response: %v", err)
|
|
}
|
|
|
|
// Empty string for next_cursor should be omitted due to omitempty tag
|
|
if bytes.Contains(data, []byte("next_cursor")) {
|
|
t.Error("empty next_cursor should be omitted from JSON")
|
|
}
|
|
}
|
|
|
|
func TestFilterFields_SingleObject(t *testing.T) {
|
|
data := map[string]interface{}{
|
|
"id": "cert-123",
|
|
"name": "My Cert",
|
|
"expiry": "2025-01-01",
|
|
"status": "active",
|
|
}
|
|
|
|
result := filterFields(data, []string{"id", "name"})
|
|
|
|
resultMap, ok := result.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("result is not map[string]interface{}, got %T", result)
|
|
}
|
|
|
|
if resultMap["id"] != "cert-123" {
|
|
t.Errorf("id = %v, want cert-123", resultMap["id"])
|
|
}
|
|
|
|
if resultMap["name"] != "My Cert" {
|
|
t.Errorf("name = %v, want My Cert", resultMap["name"])
|
|
}
|
|
|
|
if _, hasExpiry := resultMap["expiry"]; hasExpiry {
|
|
t.Error("expiry should be filtered out")
|
|
}
|
|
|
|
if _, hasStatus := resultMap["status"]; hasStatus {
|
|
t.Error("status should be filtered out")
|
|
}
|
|
}
|
|
|
|
func TestFilterFields_EmptyFields(t *testing.T) {
|
|
// Empty fields list should return data unchanged
|
|
data := map[string]interface{}{
|
|
"id": "cert-123",
|
|
"name": "My Cert",
|
|
}
|
|
|
|
result := filterFields(data, []string{})
|
|
|
|
// Should return original data unchanged
|
|
resultMap, ok := result.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("result is not map[string]interface{}, got %T", result)
|
|
}
|
|
|
|
if len(resultMap) != 2 {
|
|
t.Errorf("filtered result has %d fields, want 2", len(resultMap))
|
|
}
|
|
}
|
|
|
|
func TestFilterFields_NoMatchingFields(t *testing.T) {
|
|
data := map[string]interface{}{
|
|
"id": "cert-123",
|
|
"name": "My Cert",
|
|
}
|
|
|
|
result := filterFields(data, []string{"nonexistent", "also-not-there"})
|
|
|
|
resultMap, ok := result.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("result is not map[string]interface{}, got %T", result)
|
|
}
|
|
|
|
if len(resultMap) != 0 {
|
|
t.Errorf("filtered result has %d fields, want 0", len(resultMap))
|
|
}
|
|
}
|
|
|
|
func TestFilterFields_InvalidJSON(t *testing.T) {
|
|
// Non-serializable data should be returned as-is
|
|
data := make(chan int) // channels can't be marshaled to JSON
|
|
|
|
result := filterFields(data, []string{"field"})
|
|
|
|
// Should return original data unchanged
|
|
if result != data {
|
|
t.Error("invalid data should be returned unchanged")
|
|
}
|
|
}
|