mirror of
https://github.com/TwiN/gatus.git
synced 2026-08-24 16:16:04 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08c463542f | ||
|
|
0fd2520088 | ||
|
|
7a73be52c5 | ||
|
|
73381e4179 | ||
|
|
b7100620c3 | ||
|
|
ae7ca199aa | ||
|
|
53392dd472 | ||
|
|
4566e746f3 | ||
|
|
08895200bb | ||
|
|
809e5068d7 |
@@ -2,7 +2,9 @@ services:
|
||||
postgres:
|
||||
image: postgres
|
||||
volumes:
|
||||
- ./data/db:/var/lib/postgresql/data
|
||||
# PostgreSQL 18 and above declare the volume at /var/lib/postgresql.
|
||||
# For 17 and earlier, mount ./data/db at /var/lib/postgresql/data instead.
|
||||
- ./data/db:/var/lib/postgresql
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
ignore:
|
||||
- "storage/store/sql/specific_postgres.go" # Can't test for postgres
|
||||
- "watchdog/endpoint.go"
|
||||
- "watchdog/external_endpoint.go"
|
||||
- "watchdog/suite.go"
|
||||
- "watchdog/watchdog.go"
|
||||
comment: false
|
||||
coverage:
|
||||
status:
|
||||
patch: off
|
||||
project:
|
||||
default:
|
||||
target: 70%
|
||||
threshold: null
|
||||
|
||||
@@ -25,6 +25,6 @@ jobs:
|
||||
go-version: 1.26.3
|
||||
repository: "${{ github.event.inputs.repository || 'TwiN/gatus' }}"
|
||||
ref: "${{ github.event.inputs.ref || 'master' }}"
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- name: Benchmark
|
||||
run: go test -bench=. ./storage/store
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Set up Docker Buildx
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Get image repository
|
||||
run: echo GHCR_IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -5,7 +5,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Set up Docker Buildx
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Get image repository
|
||||
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}
|
||||
timeout-minutes: 240
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Set up Docker Buildx
|
||||
@@ -23,12 +23,12 @@ jobs:
|
||||
echo DOCKER_IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
echo GHCR_IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 240
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Set up Docker Buildx
|
||||
@@ -20,12 +20,12 @@ jobs:
|
||||
- name: Get the release
|
||||
run: echo RELEASE=${GITHUB_REF/refs\/tags\//} >> $GITHUB_ENV
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
core.setOutput('ref', pr.data.head.ref);
|
||||
core.setOutput('repo', pr.data.head.repo.full_name);
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ steps.pr.outputs.repo }}
|
||||
ref: ${{ steps.pr.outputs.ref }}
|
||||
|
||||
@@ -13,6 +13,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- run: make frontend-install
|
||||
- run: make frontend-build
|
||||
@@ -19,16 +19,11 @@ jobs:
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.26.3
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- name: Build binary to make sure it works
|
||||
run: go build
|
||||
- name: Test
|
||||
# We're using "sudo" because one of the tests leverages ping, which requires super-user privileges.
|
||||
# As for the 'env "PATH=$PATH" "GOROOT=$GOROOT"', we need it to use the same "go" executable that
|
||||
# was configured by the "Set up Go" step (otherwise, it'd use sudo's "go" executable)
|
||||
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race -coverprofile=coverage.txt -covermode=atomic
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v6.0.1
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||

|
||||
[](https://goreportcard.com/report/github.com/TwiN/gatus)
|
||||
[](https://codecov.io/gh/TwiN/gatus)
|
||||
[](https://github.com/TwiN/gatus)
|
||||
[](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
||||
[](https://github.com/TwiN)
|
||||
@@ -655,6 +654,7 @@ the client used to send the request.
|
||||
| `client.tls.renegotiation` | Type of renegotiation support to provide. (`never`, `freely`, `once`). | `"never"` |
|
||||
| `client.network` | The network to use for ICMP endpoint client (`ip`, `ip4` or `ip6`). | `"ip"` |
|
||||
| `client.tunnel` | Name of the SSH tunnel to use for this endpoint. See [Tunneling](#tunneling). | `""` |
|
||||
| `client.store-cookies` | Whether to store cookies between requests. | `false` |
|
||||
|
||||
|
||||
> 📝 Some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
|
||||
@@ -667,6 +667,7 @@ client:
|
||||
insecure: false
|
||||
ignore-redirect: false
|
||||
timeout: 10s
|
||||
store-cookies: false
|
||||
```
|
||||
|
||||
Note that this configuration is only available under `endpoints[]`, `alerting.mattermost` and `alerting.custom`.
|
||||
@@ -1303,6 +1304,10 @@ Here's an example of what the notifications look like:
|
||||
|
||||
|
||||
#### Configuring HomeAssistant alerts
|
||||
|
||||
> [!NOTE]
|
||||
> It is recommended to use the native Home Assistant integration instead (see below).
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
|:-------------------------------------------|:---------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.homeassistant.url` | HomeAssistant instance URL | Required `""` |
|
||||
@@ -1377,6 +1382,61 @@ To get your HomeAssistant long-lived access token:
|
||||
5. Give it a name (e.g., "Gatus")
|
||||
6. Copy the token - you'll only see it once!
|
||||
|
||||
##### Native Home Assistant Integration
|
||||
|
||||
Gatus can be integrated into [Home Assistant](https://www.home-assistant.io/) to monitor the status of your endpoints directly from your home automation dashboard.
|
||||
|
||||
[](https://my.home-assistant.io/redirect/config_flow_start/?domain=gatus)
|
||||
|
||||
<details>
|
||||
<summary>Configuration & Automation Examples</summary>
|
||||
|
||||
###### Configuration
|
||||
|
||||
To add the integration, click the button above or:
|
||||
|
||||
1. In Home Assistant, navigate to **Settings** > **Devices & Services**.
|
||||
2. Click **Add Integration** in the bottom-right corner.
|
||||
3. Search for **Gatus** and select it.
|
||||
4. Enter the base URL of your Gatus instance (e.g., `http://192.168.1.50:8080`).
|
||||
|
||||
Home Assistant will create a binary sensor for each endpoint configured in Gatus. Because these sensors use the `connectivity` device class, they will show as `Connected` (Up) when the endpoint is healthy, and `Disconnected` (Down) when the endpoint is unhealthy.
|
||||
|
||||
###### Automation Example
|
||||
|
||||
You can easily set up a UI automation in Home Assistant to receive mobile notifications if any endpoint goes down:
|
||||
|
||||
1. Navigate to **Settings** > **Automations & Scenes** > **Create Automation** > **Create new automation**.
|
||||
2. Under **Triggers**, click **Add Trigger** and select **State**.
|
||||
3. In the **Entity** field, select the Gatus binary sensor(s) you want to monitor.
|
||||
4. Set the **To** field to `off` (since a state of `off` represents a disconnected/down endpoint).
|
||||
5. Under **Actions**, click **Add Action** and select **Perform action**.
|
||||
6. Select your notification service (e.g., `Send a notification via mobile_app_<your_device_name>`).
|
||||
7. Set the **Message** field. You can use template values to dynamically include the endpoint name:
|
||||
```yaml
|
||||
message: "The endpoint {{ trigger.to_state.name }} is down!"
|
||||
```
|
||||
|
||||
For advanced users, here is the YAML representation of the automation:
|
||||
|
||||
```yaml
|
||||
alias: "Notify when Gatus endpoint goes down"
|
||||
description: "Sends a mobile notification if a Gatus endpoint goes down"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- binary_sensor.website
|
||||
- binary_sensor.make_sure_header_is_rendered
|
||||
to: "off"
|
||||
action:
|
||||
- action: notify.mobile_app_your_device_name
|
||||
data:
|
||||
title: "Gatus Alert"
|
||||
message: "The endpoint {{ trigger.to_state.name }} is down!"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
#### Configuring IFTTT alerts
|
||||
|
||||
@@ -2907,6 +2967,7 @@ To get more details, please check [chart's configuration](https://github.com/Twi
|
||||
|
||||
Gatus can be deployed on Kubernetes using Terraform by using the following module: [terraform-kubernetes-gatus](https://github.com/TwiN/terraform-kubernetes-gatus).
|
||||
|
||||
|
||||
## Running the tests
|
||||
```console
|
||||
go test -v ./...
|
||||
|
||||
@@ -21,6 +21,7 @@ func isIgnorableTestError(err error) bool {
|
||||
return strings.Contains(errStr, "user does not exist") ||
|
||||
strings.Contains(errStr, "no such host") ||
|
||||
strings.Contains(errStr, "invalid username, password or token") ||
|
||||
strings.Contains(errStr, "unknown API error") ||
|
||||
strings.Contains(errStr, "dial tcp")
|
||||
}
|
||||
|
||||
|
||||
@@ -499,6 +499,13 @@ func QueryDNS(queryType, queryName, url string) (connected bool, dnsRcode string
|
||||
if srv, ok := rr.(*dns.SRV); ok {
|
||||
body = []byte(fmt.Sprintf("%s:%d", srv.Target, srv.Port))
|
||||
}
|
||||
case dns.TypeTXT:
|
||||
if txt, ok := rr.(*dns.TXT); ok {
|
||||
if len(body) > 0 {
|
||||
body = append(body, '\n')
|
||||
}
|
||||
body = append(body, []byte(strings.Join(txt.Txt, ""))...)
|
||||
}
|
||||
default:
|
||||
body = []byte("query type is not supported yet")
|
||||
}
|
||||
|
||||
+30
-2
@@ -8,6 +8,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -16,6 +17,17 @@ import (
|
||||
"github.com/TwiN/gatus/v5/test"
|
||||
)
|
||||
|
||||
func isIgnorableNetworkTestError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
errString := err.Error()
|
||||
return strings.Contains(errString, "no such host") ||
|
||||
strings.Contains(errString, "connection reset by peer") ||
|
||||
strings.Contains(errString, "i/o timeout") ||
|
||||
strings.Contains(errString, "server misbehaving")
|
||||
}
|
||||
|
||||
func TestGetHTTPClient(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Insecure: false,
|
||||
@@ -221,6 +233,9 @@ func TestCanPerformStartTLS(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
connected, _, err := CanPerformStartTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second, DNSResolver: tt.args.dnsresolver})
|
||||
if !tt.wantErr && isIgnorableNetworkTestError(err) {
|
||||
t.Skipf("skipping due to transient network error: %v", err)
|
||||
}
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
||||
return
|
||||
@@ -290,6 +305,9 @@ func TestCanPerformTLS(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
connected, _, _, err := CanPerformTLS(tt.args.address, "", &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
|
||||
if !tt.wantErr && isIgnorableNetworkTestError(err) {
|
||||
t.Skipf("skipping due to transient network error: %v", err)
|
||||
}
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CanPerformTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
||||
return
|
||||
@@ -507,6 +525,16 @@ func TestQueryDNS(t *testing.T) {
|
||||
expectedDNSCode: "NOERROR",
|
||||
expectedBody: "one.one.one.one.",
|
||||
},
|
||||
{
|
||||
name: "test Config with type TXT",
|
||||
inputDNS: dns.Config{
|
||||
QueryType: "TXT",
|
||||
QueryName: "example.com.",
|
||||
},
|
||||
inputURL: "1.1.1.1",
|
||||
expectedDNSCode: "NOERROR",
|
||||
expectedBody: "*v=spf1*",
|
||||
},
|
||||
{
|
||||
name: "test Config with fake type and retrieve error",
|
||||
inputDNS: dns.Config{
|
||||
@@ -526,8 +554,8 @@ func TestQueryDNS(t *testing.T) {
|
||||
if dnsRCode != scenario.expectedDNSCode {
|
||||
t.Errorf("expected DNSRCode to be %s, got %s", scenario.expectedDNSCode, dnsRCode)
|
||||
}
|
||||
if scenario.inputDNS.QueryType == "NS" {
|
||||
// Because there are often multiple nameservers backing a single domain, we'll only look at the suffix
|
||||
if scenario.inputDNS.QueryType == "NS" || scenario.inputDNS.QueryType == "TXT" {
|
||||
// Some record types can have multiple valid answers, so wildcard matching is used in those scenarios
|
||||
if !pattern.Match(scenario.expectedBody, string(body)) {
|
||||
t.Errorf("got %s, expected result %s,", string(body), scenario.expectedBody)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -34,6 +35,7 @@ var (
|
||||
IgnoreRedirect: false,
|
||||
Timeout: defaultTimeout,
|
||||
Network: "ip",
|
||||
StoreCookies: false,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -82,6 +84,9 @@ type Config struct {
|
||||
// ResolvedTunnel is the resolved SSH tunnel for this specific Config
|
||||
ResolvedTunnel *sshtunnel.SSHTunnel `yaml:"-"`
|
||||
|
||||
// StoreCookies determines whether cookies are stored and included across requests.
|
||||
StoreCookies bool `yaml:"store-cookies,omitempty"`
|
||||
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
@@ -280,6 +285,13 @@ func (c *Config) getHTTPClient() *http.Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.StoreCookies {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
logr.Fatalf("[client.getHTTPClient] Failed to initialize cookie jar: %v", err)
|
||||
}
|
||||
c.httpClient.Jar = jar
|
||||
}
|
||||
}
|
||||
return c.httpClient
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ func TestConfig_getHTTPClient(t *testing.T) {
|
||||
if insecureClient.Timeout != defaultTimeout {
|
||||
t.Error("expected Config.Timeout to default the HTTP client to a timeout of 10s")
|
||||
}
|
||||
if insecureClient.Jar != nil {
|
||||
t.Error("expected Config.StoreCookies to default the HTTP client to not store cookies")
|
||||
}
|
||||
request, _ := http.NewRequest("GET", "", nil)
|
||||
if err := insecureClient.CheckRedirect(request, nil); err != nil {
|
||||
t.Error("expected Config.IgnoreRedirect set to false to cause the HTTP client's CheckRedirect to return nil")
|
||||
@@ -31,12 +34,24 @@ func TestConfig_getHTTPClient(t *testing.T) {
|
||||
if secureClient.Timeout != 5*time.Second {
|
||||
t.Error("expected Config.Timeout to cause the HTTP client to have a timeout of 5s")
|
||||
}
|
||||
if insecureClient.Jar != nil {
|
||||
t.Error("expected Config.StoreCookies to default the HTTP client to not store cookies")
|
||||
}
|
||||
request, _ = http.NewRequest("GET", "", nil)
|
||||
if err := secureClient.CheckRedirect(request, nil); err != http.ErrUseLastResponse {
|
||||
t.Error("expected Config.IgnoreRedirect set to true to cause the HTTP client's CheckRedirect to return http.ErrUseLastResponse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_getHTTPClient_withCookieJar(t *testing.T) {
|
||||
config := &Config{StoreCookies: true}
|
||||
config.ValidateAndSetDefaults()
|
||||
client := config.getHTTPClient()
|
||||
if client.Jar == nil {
|
||||
t.Error("expected Config.StoreCookies to cause the HTTP client to have a CookieJar")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_ValidateAndSetDefaults_withCustomDNSResolver(t *testing.T) {
|
||||
type args struct {
|
||||
dnsResolver string
|
||||
|
||||
@@ -27,7 +27,7 @@ require (
|
||||
github.com/registrobr/rdap v1.1.8
|
||||
github.com/valyala/fasthttp v1.71.0
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.2
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/api v0.280.0
|
||||
@@ -91,7 +91,7 @@ require (
|
||||
golang.org/x/image v0.40.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
||||
|
||||
@@ -188,8 +188,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
@@ -232,8 +232,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
||||
@@ -87,7 +87,7 @@ func extractValue(currentKey string, value interface{}) interface{} {
|
||||
}
|
||||
}
|
||||
arrayIndex, err := strconv.Atoi(index)
|
||||
if err != nil {
|
||||
if err != nil || arrayIndex < 0 {
|
||||
return nil
|
||||
}
|
||||
currentKeyWithoutIndex := currentKey[:startOfBracket]
|
||||
|
||||
@@ -174,6 +174,38 @@ func TestEval(t *testing.T) {
|
||||
ExpectedOutputLength: 18,
|
||||
ExpectedError: false,
|
||||
},
|
||||
{
|
||||
Name: "negative-index-on-keyed-array",
|
||||
Path: "data[-1]",
|
||||
Data: `{"data": [1, 2, 3]}`,
|
||||
ExpectedOutput: "",
|
||||
ExpectedOutputLength: 0,
|
||||
ExpectedError: true,
|
||||
},
|
||||
{
|
||||
Name: "negative-index-on-root-array",
|
||||
Path: "[-1]",
|
||||
Data: `[1, 2, 3]`,
|
||||
ExpectedOutput: "",
|
||||
ExpectedOutputLength: 0,
|
||||
ExpectedError: true,
|
||||
},
|
||||
{
|
||||
Name: "negative-index-followed-by-key",
|
||||
Path: "data[-1].name",
|
||||
Data: `{"data": [{"name": "value"}]}`,
|
||||
ExpectedOutput: "",
|
||||
ExpectedOutputLength: 0,
|
||||
ExpectedError: true,
|
||||
},
|
||||
{
|
||||
Name: "negative-index-nested-array",
|
||||
Path: "data[0][-1]",
|
||||
Data: `{"data": [[1, 2, 3]]}`,
|
||||
ExpectedOutput: "",
|
||||
ExpectedOutputLength: 0,
|
||||
ExpectedError: true,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user