Files
probo/pkg/connector/provider/probe_test.go
Sacha Al Himdani 4c57d201a4 Make license declarations consistently MIT
The source headers, LICENSE files, and license metadata had drifted
apart. Align the entire project to MIT:

- Convert every source-file header to the MIT text across all comment
  styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including
  SPDX-License-Identifier tags
- Set the root and cookie-banner LICENSE files to the MIT text with a
  "MIT License" title line
- Switch the package.json license fields, Docker image label, and
  cookie-banner README to MIT
- Update docs and the genmodels header generator accordingly
- Normalize copyright lines to a single format
  (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the
  hello@getprobo.com and hello@probo.inc emails to hello@probo.com and
  the comma-separated years to a hyphenated range

Genuine third-party references are intentionally left untouched: the
Lucide icon attributions (Lucide is ISC) and the trivy dependency
license allowlist.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-13 16:21:14 +02:00

323 lines
9.8 KiB
Go

// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package provider
import (
"context"
"io"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/pkg/coredata"
)
// probeRoundTripFunc lets a test capture the probe request and return a
// canned response without touching the network.
type probeRoundTripFunc func(*http.Request) (*http.Response, error)
func (f probeRoundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
func TestBuiltinRegistry_ProbeCoverage(t *testing.T) {
t.Parallel()
r := NewBuiltinRegistry()
for _, reg := range r.All() {
hasProbe := reg.Probe != nil || reg.ProbeURL != "" || reg.BuildProbeURL != nil
assert.True(t, hasProbe, "provider %s has no connection probe configured", reg.Provider)
}
}
func TestBuildDatadogProbeURL(t *testing.T) {
t.Parallel()
conn := &coredata.Connector{Provider: coredata.ConnectorProviderDatadog}
require.NoError(t, conn.SetSettings(&coredata.DatadogConnectorSettings{
Domain: "us3.datadoghq.com",
Region: "US3",
}))
probeURL, err := buildDatadogProbeURL(conn)
require.NoError(t, err)
assert.Equal(
t,
"https://api.us3.datadoghq.com/api/v2/users?page%5Bnumber%5D=0&page%5Bsize%5D=1",
probeURL,
)
}
func TestBuildZendeskProbeURL(t *testing.T) {
t.Parallel()
conn := &coredata.Connector{Provider: coredata.ConnectorProviderZendesk}
require.NoError(t, conn.SetSettings(&coredata.ZendeskConnectorSettings{
Subdomain: "acme",
}))
probeURL, err := buildZendeskProbeURL(conn)
require.NoError(t, err)
assert.Contains(t, probeURL, "https://acme.zendesk.com/api/v2/users.json")
}
func TestBuildOktaProbeURL(t *testing.T) {
t.Parallel()
conn := &coredata.Connector{Provider: coredata.ConnectorProviderOkta}
require.NoError(t, conn.SetSettings(&coredata.OktaConnectorSettings{
Domain: "acme.okta.com",
}))
probeURL, err := buildOktaProbeURL(conn)
require.NoError(t, err)
assert.Equal(t, "https://acme.okta.com/api/v1/users?limit=1", probeURL)
}
func TestBuildLangfuseProbeURL(t *testing.T) {
t.Parallel()
conn := &coredata.Connector{Provider: coredata.ConnectorProviderLangfuse}
require.NoError(t, conn.SetSettings(&coredata.LangfuseConnectorSettings{
BaseURL: "https://us.cloud.langfuse.com",
}))
probeURL, err := buildLangfuseProbeURL(conn)
require.NoError(t, err)
assert.Equal(t, "https://us.cloud.langfuse.com/api/public/organizations/memberships", probeURL)
}
func TestBuildPostHogProbeURL(t *testing.T) {
t.Parallel()
conn := &coredata.Connector{Provider: coredata.ConnectorProviderPostHog}
require.NoError(t, conn.SetSettings(&coredata.PostHogConnectorSettings{
BaseURL: "https://us.posthog.com",
}))
probeURL, err := buildPostHogProbeURL(conn)
require.NoError(t, err)
assert.Equal(t, "https://us.posthog.com/api/organizations/@current/", probeURL)
}
func TestBuildScalewayProbeURL(t *testing.T) {
t.Parallel()
conn := &coredata.Connector{Provider: coredata.ConnectorProviderScaleway}
require.NoError(t, conn.SetSettings(&coredata.ScalewayConnectorSettings{
OrganizationID: "11111111-2222-3333-4444-555555555555",
}))
probeURL, err := buildScalewayProbeURL(conn)
require.NoError(t, err)
assert.Equal(
t,
"https://api.scaleway.com/iam/v1alpha1/users?organization_id=11111111-2222-3333-4444-555555555555&page_size=1",
probeURL,
)
}
func TestProbeOpenRouter(t *testing.T) {
t.Parallel()
// probeOpenRouter must reject 401/403 (bad key) and 404 (a valid but
// personal/non-organization key, which the members endpoint rejects with
// 404), while letting 2xx pass.
cases := []struct {
name string
status int
wantReject bool
}{
{"valid management key", http.StatusOK, false},
{"revoked key", http.StatusUnauthorized, true},
{"forbidden key", http.StatusForbidden, true},
{"personal (non-org) key", http.StatusNotFound, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var gotURL string
client := &http.Client{Transport: probeRoundTripFunc(func(r *http.Request) (*http.Response, error) {
gotURL = r.URL.String()
return &http.Response{StatusCode: tc.status, Body: http.NoBody, Header: make(http.Header)}, nil
})}
err := probeOpenRouter(context.Background(), client, &coredata.Connector{Provider: coredata.ConnectorProviderOpenRouter})
assert.Equal(t, "https://openrouter.ai/api/v1/organization/members?limit=1", gotURL)
if tc.wantReject {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestProbeHeroku(t *testing.T) {
t.Parallel()
// The fix's contract: probeHeroku must send Heroku's versioned Accept
// header — a plain "application/json" returns 400, which doProbeRequest
// reads as connected and masks a dead token — and it must map 401/403 to
// a rejection while letting 2xx pass.
cases := []struct {
name string
status int
wantReject bool
}{
{"valid credential", http.StatusOK, false},
{"revoked credential", http.StatusUnauthorized, true},
{"forbidden credential", http.StatusForbidden, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var gotAccept, gotURL string
client := &http.Client{Transport: probeRoundTripFunc(func(r *http.Request) (*http.Response, error) {
gotAccept = r.Header.Get("Accept")
gotURL = r.URL.String()
return &http.Response{StatusCode: tc.status, Body: http.NoBody, Header: make(http.Header)}, nil
})}
err := probeHeroku(context.Background(), client, &coredata.Connector{Provider: coredata.ConnectorProviderHeroku})
assert.Equal(t, "application/vnd.heroku+json; version=3", gotAccept)
assert.Equal(t, "https://api.heroku.com/account", gotURL)
if tc.wantReject {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestProbeRailway(t *testing.T) {
t.Parallel()
// Railway returns HTTP 200 with a populated errors array (data.me null) for
// a rejected token instead of 401/403, so the probe must inspect the body —
// the generic 401/403-only contract would falsely accept a dead token.
cases := []struct {
name string
status int
body string
wantReject bool
}{
{"valid token", http.StatusOK, `{"data":{"me":{"id":"u-1"}}}`, false},
{"rejected token (200 + errors)", http.StatusOK, `{"errors":[{"message":"Not Authorized"}],"data":null}`, true},
{"null me", http.StatusOK, `{"data":{"me":null}}`, true},
{"unauthorized status", http.StatusUnauthorized, ``, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var gotURL, gotContentType string
client := &http.Client{Transport: probeRoundTripFunc(func(r *http.Request) (*http.Response, error) {
gotURL = r.URL.String()
gotContentType = r.Header.Get("Content-Type")
return &http.Response{
StatusCode: tc.status,
Body: io.NopCloser(strings.NewReader(tc.body)),
Header: make(http.Header),
}, nil
})}
err := probeRailway(context.Background(), client, &coredata.Connector{Provider: coredata.ConnectorProviderRailway})
assert.Equal(t, "https://backboard.railway.com/graphql/v2", gotURL)
assert.Equal(t, "application/json", gotContentType)
if tc.wantReject {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestProbeCrisp(t *testing.T) {
t.Parallel()
// probeCrisp must send the non-auth X-Crisp-Tier header (the generic
// probeGET does not) and hit the configured website's operators/list
// endpoint; 401/403 mean a rejected credential, and 404 means a valid token
// pointed at a wrong/unbound website_id — a permanent misconfiguration that
// must be rejected at connect time rather than fail every later review.
cases := []struct {
name string
status int
wantReject bool
}{
{"valid token", http.StatusOK, false},
{"revoked token", http.StatusUnauthorized, true},
{"forbidden token", http.StatusForbidden, true},
{"wrong or unbound website (404)", http.StatusNotFound, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
conn := &coredata.Connector{Provider: coredata.ConnectorProviderCrisp}
require.NoError(t, conn.SetSettings(&coredata.CrispConnectorSettings{WebsiteID: "abc-123"}))
var gotURL, gotTier string
client := &http.Client{Transport: probeRoundTripFunc(func(r *http.Request) (*http.Response, error) {
gotURL = r.URL.String()
gotTier = r.Header.Get("X-Crisp-Tier")
return &http.Response{StatusCode: tc.status, Body: http.NoBody, Header: make(http.Header)}, nil
})}
err := probeCrisp(context.Background(), client, conn)
assert.Equal(t, "https://api.crisp.chat/v1/website/abc-123/operators/list", gotURL)
assert.Equal(t, "plugin", gotTier)
if tc.wantReject {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}