Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
ci:
name: ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
cache: true

- name: golangci-lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.11.3

- name: go build
run: go build ./...

- name: go test
run: go test -race ./...
25 changes: 25 additions & 0 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Dependabot auto-merge

on: pull_request

permissions:
contents: write
pull-requests: write

jobs:
automerge:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v3.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Enable auto-merge for patch and minor updates
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 changes: 30 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: "2"

run:
timeout: 5m

linters:
default: standard
enable:
- gocritic
- misspell
- unconvert
settings:
errcheck:
exclude-functions:
- fmt.Fprint
- fmt.Fprintf
- fmt.Fprintln
- (*text/tabwriter.Writer).Flush
- (io.Closer).Close
exclusions:
rules:
- path: _test\.go
linters:
- errcheck
- gosec

formatters:
enable:
- gofmt
- goimports
16 changes: 16 additions & 0 deletions .pinact.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/pinact/refs/heads/main/json-schema/pinact.json
# pinact - https://github.com/suzuki-shunsuke/pinact
version: 3
# files:
# - pattern: action.yaml
# - pattern: */action.yaml

# separator: " # "

ignore_actions:
# - name: slsa-framework/slsa-github-generator/\.github/workflows/generator_generic_slsa3\.yml
# ref: v\d+\.\d+\.\d+
# - name: actions/.*
# ref: main
# - name: suzuki-shunsuke/.*
# ref: release-.*
99 changes: 99 additions & 0 deletions cmd/document_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package cmd

import (
"io"
"os"
"path/filepath"
"strings"
"testing"
)

func TestLoadSearchBody_Empty(t *testing.T) {
b, err := loadSearchBody("")
if err != nil {
t.Fatalf("err = %v", err)
}
if b != nil {
t.Errorf("body = %q, want nil", string(b))
}
}

func TestLoadSearchBody_InlineObject(t *testing.T) {
b, err := loadSearchBody(`{"title":"x"}`)
if err != nil {
t.Fatalf("err = %v", err)
}
if string(b) != `{"title":"x"}` {
t.Errorf("body = %s", string(b))
}
}

func TestLoadSearchBody_InlineArray(t *testing.T) {
b, err := loadSearchBody(`[1,2,3]`)
if err != nil {
t.Fatalf("err = %v", err)
}
if string(b) != `[1,2,3]` {
t.Errorf("body = %s", string(b))
}
}

func TestLoadSearchBody_File(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "body.json")
content := `{"form_name":"経費"}`
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
b, err := loadSearchBody(path)
if err != nil {
t.Fatalf("err = %v", err)
}
if string(b) != content {
t.Errorf("body = %q", string(b))
}
}

func TestLoadSearchBody_FileMissing(t *testing.T) {
_, err := loadSearchBody(filepath.Join(t.TempDir(), "nope.json"))
if err == nil || !strings.Contains(err.Error(), "read --body file") {
t.Errorf("err = %v", err)
}
}

func TestLoadSearchBody_InvalidJSON(t *testing.T) {
_, err := loadSearchBody(`{not json}`)
if err == nil || !strings.Contains(err.Error(), "not valid JSON") {
t.Errorf("err = %v", err)
}
}

func TestLoadSearchBody_Stdin(t *testing.T) {
orig := os.Stdin
t.Cleanup(func() { os.Stdin = orig })

r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
os.Stdin = r

want := `{"from":"stdin"}`
done := make(chan error, 1)
go func() {
_, werr := io.WriteString(w, want)
_ = w.Close()
done <- werr
}()

b, err := loadSearchBody("-")
if werr := <-done; werr != nil {
t.Fatalf("write to stdin: %v", werr)
}
if err != nil {
t.Fatalf("loadSearchBody: %v", err)
}
if string(b) != want {
t.Errorf("body = %q", string(b))
}
}
157 changes: 157 additions & 0 deletions cmd/output_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package cmd

import (
"bytes"
"encoding/json"
"io"
"os"
"strings"
"testing"
)

func captureStdout(t *testing.T, fn func() error) (string, error) {
t.Helper()
orig := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
os.Stdout = w

done := make(chan struct{})
var buf bytes.Buffer
go func() {
_, _ = io.Copy(&buf, r)
close(done)
}()

runErr := fn()
_ = w.Close()
<-done
os.Stdout = orig
return buf.String(), runErr
}

func TestResolveOutputFormat_ExplicitFlagWins(t *testing.T) {
if got := resolveOutputFormat("json"); got != "json" {
t.Errorf("resolveOutputFormat(\"json\") = %q", got)
}
if got := resolveOutputFormat("table"); got != "table" {
t.Errorf("resolveOutputFormat(\"table\") = %q", got)
}
}

func TestResolveOutputFormat_NonTTYDefaultsToJSON(t *testing.T) {
// In `go test`, stdout is not a TTY, so the default is "json".
if got := resolveOutputFormat(""); got != "json" {
t.Errorf("default = %q, want json", got)
}
}

func TestRunJQ_SimpleFilter(t *testing.T) {
input := map[string]any{
"form_group": []any{
map[string]any{"id": 1.0, "name": "g1"},
map[string]any{"id": 2.0, "name": "g2"},
},
}
out, err := captureStdout(t, func() error {
return runJQ(input, ".form_group[].name")
})
if err != nil {
t.Fatalf("runJQ: %v", err)
}
lines := strings.Split(strings.TrimSpace(out), "\n")
if len(lines) != 2 {
t.Fatalf("lines = %d, want 2: %q", len(lines), out)
}
// Each line is JSON-encoded; parse to compare the string value.
for i, want := range []string{"g1", "g2"} {
var got string
if err := json.Unmarshal([]byte(lines[i]), &got); err != nil {
t.Fatalf("line[%d] not JSON: %v", i, err)
}
if got != want {
t.Errorf("line[%d] = %q, want %q", i, got, want)
}
}
}

func TestRunJQ_InvalidExpression(t *testing.T) {
err := runJQ(map[string]any{}, ".[")
if err == nil {
t.Fatal("expected parse error, got nil")
}
if !strings.Contains(err.Error(), "invalid --jq filter") {
t.Errorf("error = %v", err)
}
}

func TestRunJQ_RuntimeError(t *testing.T) {
// Dividing a string triggers a runtime jq error.
err := runJQ(map[string]any{"v": "abc"}, ".v / 2")
if err == nil {
t.Fatal("expected runtime error, got nil")
}
if !strings.Contains(err.Error(), "jq error") {
t.Errorf("error = %v", err)
}
}

func TestRender_JSONPath(t *testing.T) {
payload := map[string]any{"k": "v"}
out, err := captureStdout(t, func() error {
return render(payload, "json", "", func() error {
t.Error("table fn should not be called when format=json")
return nil
})
})
if err != nil {
t.Fatalf("render: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal([]byte(out), &decoded); err != nil {
t.Fatalf("output not JSON: %v (%s)", err, out)
}
if decoded["k"] != "v" {
t.Errorf("decoded = %v", decoded)
}
}

func TestRender_TablePath(t *testing.T) {
called := false
_, err := captureStdout(t, func() error {
return render("unused", "table", "", func() error {
called = true
return nil
})
})
if err != nil {
t.Fatalf("render: %v", err)
}
if !called {
t.Error("table fn was not invoked")
}
}

func TestRender_JQTakesPrecedence(t *testing.T) {
out, err := captureStdout(t, func() error {
return render(map[string]any{"k": "v"}, "table", ".k", func() error {
t.Error("table fn should not be called when --jq is set")
return nil
})
})
if err != nil {
t.Fatalf("render: %v", err)
}
if strings.TrimSpace(out) != `"v"` {
t.Errorf("output = %q, want \"v\"", out)
}
}

func TestRender_UnknownFormat(t *testing.T) {
err := render("x", "yaml", "", func() error { return nil })
if err == nil || !strings.Contains(err.Error(), "unknown output format") {
t.Errorf("err = %v", err)
}
}
Loading