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
45 changes: 45 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Integration Tests

# Manual-only: triggers paid DO API calls. Never runs on every PR.
on:
workflow_dispatch:
inputs:
app_name:
description: 'Name of a pre-existing App Platform app to use for trusted_sources resolution test'
required: true
type: string

# Restrict GITHUB_TOKEN to the minimum permissions needed.
# This workflow only checks out code and runs tests — it never writes back
# to the repository or publishes packages.
permissions:
contents: read

env:
GOPRIVATE: github.com/GoCodeAlone/*

jobs:
trusted-sources-app-resolution:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26'
- name: Verify DIGITALOCEAN_TOKEN is configured
env:
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
run: |
if [ -z "$DIGITALOCEAN_TOKEN" ]; then
echo "::error::DIGITALOCEAN_TOKEN secret is not set — configure it in repository Settings → Secrets before running this workflow"
exit 1
fi
- name: Configure Git for private repos
run: git config --global url."https://x-access-token:${{ secrets.RELEASES_TOKEN }}@github.com/".insteadOf "https://github.com/"
- name: Run trusted_sources live integration test
env:
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
DO_TEST_APP_NAME: ${{ inputs.app_name }}
run: |
Comment on lines +39 to +43
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow can succeed without actually running the live test when DIGITALOCEAN_TOKEN is missing, because the test uses t.Skip on empty token. Consider adding an explicit preflight check in the workflow step (or a separate step) that fails early if secrets.DIGITALOCEAN_TOKEN is not set, to avoid a false-green manual run.

Copilot uses AI. Check for mistakes.
GOWORK=off go test -v -tags integration ./internal/drivers/... \
-run TestDatabaseDriver_TrustedSources_AppNameResolution_Live
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
103 changes: 103 additions & 0 deletions internal/drivers/database_trusted_sources_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//go:build integration

package drivers

// Live integration test for trusted_sources app name→UUID resolution.
//
// Requirements:
// DIGITALOCEAN_TOKEN — personal access token with Apps read scope
// DO_TEST_APP_NAME — name of a pre-existing App Platform app in the account
//
// Both variables must be set or the test skips with t.Skip.
// The test is read-only: it never creates, modifies, or deletes any resource.
//
// Run manually:
// DIGITALOCEAN_TOKEN=<tok> DO_TEST_APP_NAME=<name> \
// GOWORK=off go test -v -tags integration ./internal/drivers/... \
// -run TestDatabaseDriver_TrustedSources_AppNameResolution_Live

import (
"context"
"os"
"testing"
"time"

"github.com/digitalocean/godo"
"golang.org/x/oauth2"
)

// TestDatabaseDriver_TrustedSources_AppNameResolution_Live calls
// resolveAppNamesMap against the live DO Apps API and verifies that:
// 1. The function resolves DO_TEST_APP_NAME to a UUID without error.
// 2. The returned UUID is UUID-shaped (passes isUUIDLike).
// 3. The UUID matches what an independent Apps.List call returns for the same
// app, confirming there is no off-by-one or pagination bug.
func TestDatabaseDriver_TrustedSources_AppNameResolution_Live(t *testing.T) {
token := os.Getenv("DIGITALOCEAN_TOKEN")
if token == "" {
t.Skip("DIGITALOCEAN_TOKEN not set — skipping live integration test")
}
appName := os.Getenv("DO_TEST_APP_NAME")
if appName == "" {
t.Skip("DO_TEST_APP_NAME not set — skipping live integration test")
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
httpClient := oauth2.NewClient(ctx, ts)
godoClient := godo.NewClient(httpClient)

d := &DatabaseDriver{
appsClient: godoClient.Apps,
region: "nyc3",
}

// ── 1. Resolve via the function under test ────────────────────────────────
raw := []any{
map[string]any{"type": "app", "value": appName},
}
resolved, err := d.resolveAppNamesMap(ctx, raw)
if err != nil {
t.Fatalf("resolveAppNamesMap(%q): %v", appName, err)
}

gotUUID, ok := resolved[appName]
if !ok {
t.Fatalf("resolveAppNamesMap result missing key %q; map: %v", appName, resolved)
}
if !isUUIDLike(gotUUID) {
t.Errorf("resolved value %q for app %q does not look like a UUID", gotUUID, appName)
}

// ── 2. Independent cross-check via Apps.List ──────────────────────────────
wantUUID := ""
opts := &godo.ListOptions{Page: 1, PerPage: 200}
for {
apps, resp, listErr := godoClient.Apps.List(ctx, opts)
if listErr != nil {
t.Fatalf("Apps.List (cross-check): %v", listErr)
}
for _, app := range apps {
if app.Spec != nil && app.Spec.Name == appName {
wantUUID = app.ID
break
}
}
if wantUUID != "" || resp == nil || resp.Links == nil || resp.Links.IsLastPage() {
break
}
opts.Page++
}
if wantUUID == "" {
t.Fatalf("app %q not found in Apps.List; verify DO_TEST_APP_NAME is correct", appName)
}

// ── 3. Assert resolved UUID == cross-checked UUID ─────────────────────────
if gotUUID != wantUUID {
t.Errorf("UUID mismatch for app %q:\n resolveAppNamesMap → %q\n Apps.List → %q",
appName, gotUUID, wantUUID)
}
t.Logf("✓ app %q resolved to UUID %q", appName, gotUUID)
}
Loading