internal/encoder/json/encoder_test.go

package json_test

import (
	"bytes"
	stdjson "encoding/json"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"mercemay.top/src/lambdalog/internal/encoder"
	logjson "mercemay.top/src/lambdalog/internal/encoder/json"
)

func TestEncoder_Name(t *testing.T) {
	if got := (logjson.Encoder{}).Name(); got != "json" {
		t.Fatalf("Name = %q, want json", got)
	}
}

func TestEncoder_Encode(t *testing.T) {
	cases := []struct {
		name string
		rec  encoder.Record
		want map[string]any
	}{
		{
			name: "basic",
			rec: encoder.Record{
				Time:    time.Date(2024, 10, 1, 12, 0, 0, 0, time.UTC),
				Level:   "info",
				Message: "hello",
			},
			want: map[string]any{
				"time":  "2024-10-01T12:00:00Z",
				"level": "info",
				"msg":   "hello",
			},
		},
		{
			name: "with-request-id-and-fields",
			rec: encoder.Record{
				Time:      time.Date(2024, 10, 1, 12, 0, 0, 0, time.UTC),
				Level:     "error",
				Message:   "boom",
				RequestID: "req-1",
				Fields: []encoder.Field{
					{Key: "count", Value: int64(3)},
					{Key: "ok", Value: false},
				},
			},
			want: map[string]any{
				"time":       "2024-10-01T12:00:00Z",
				"level":      "error",
				"msg":        "boom",
				"request_id": "req-1",
				"count":      float64(3),
				"ok":         false,
			},
		},
	}

	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Helper()
			var buf bytes.Buffer
			if err := (logjson.Encoder{}).Encode(&buf, tc.rec); err != nil {
				t.Fatalf("encode: %v", err)
			}
			if !strings.HasSuffix(buf.String(), "\n") {
				t.Fatalf("missing trailing newline: %q", buf.String())
			}
			got := map[string]any{}
			if err := stdjson.Unmarshal(buf.Bytes(), &got); err != nil {
				t.Fatalf("parse: %v: %s", err, buf.String())
			}
			if diff := cmp.Diff(tc.want, got); diff != "" {
				t.Fatalf("record (-want +got):\n%s", diff)
			}
		})
	}
}

func TestEncoder_EscapesQuotes(t *testing.T) {
	var buf bytes.Buffer
	rec := encoder.Record{Message: `he said "hi"`, Level: "info", Time: time.Unix(0, 0).UTC()}
	if err := (logjson.Encoder{}).Encode(&buf, rec); err != nil {
		t.Fatalf("encode: %v", err)
	}
	out := map[string]any{}
	if err := stdjson.Unmarshal(buf.Bytes(), &out); err != nil {
		t.Fatalf("parse: %v", err)
	}
	if out["msg"] != `he said "hi"` {
		t.Fatalf("round-trip lost quoting: %#v", out["msg"])
	}
}

func TestEncoder_TimeFormatOverride(t *testing.T) {
	enc := logjson.Encoder{TimeFormat: time.RFC3339}
	var buf bytes.Buffer
	rec := encoder.Record{Time: time.Date(2024, 1, 2, 3, 4, 5, 999, time.UTC), Level: "info", Message: "x"}
	if err := enc.Encode(&buf, rec); err != nil {
		t.Fatalf("encode: %v", err)
	}
	if !strings.Contains(buf.String(), "2024-01-02T03:04:05Z") {
		t.Fatalf("expected RFC3339 time, got %s", buf.String())
	}
}