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
25 changes: 19 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,33 @@ jobs:

contract:
name: contract schema (L2)
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Checkout contract
uses: actions/checkout@v4
with:
repository: openbootdotdev/openboot-contract
ref: ${{ github.event.client_payload.ref || 'main' }}
path: .contract

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"

- name: Contract schema validation
run: |
git clone --depth 1 https://github.com/openbootdotdev/openboot-contract.git /tmp/contract
pip3 install --break-system-packages jsonschema

python3 -c "
import json, jsonschema, sys

checks = [
('/tmp/contract/schemas/remote-config.json', '/tmp/contract/fixtures/config-v1.json'),
('/tmp/contract/schemas/snapshot.json', '/tmp/contract/fixtures/snapshot-v1.json'),
('.contract/schemas/remote-config.json', '.contract/fixtures/config-v1.json'),
('.contract/schemas/snapshot.json', '.contract/fixtures/snapshot-v1.json'),
]

failed = 0
Expand All @@ -103,9 +113,13 @@ jobs:
sys.exit(1 if failed else 0)
"

- name: Verify CLI consumes canonical fixtures losslessly
env:
OPENBOOT_CONTRACT_DIR: ${{ github.workspace }}/.contract
run: go test -v -tags=contract ./test/contract

curl-bash-smoke:
name: curl|bash smoke
if: github.event_name != 'pull_request'
runs-on: macos-latest
steps:
- name: Checkout code
Expand Down Expand Up @@ -137,7 +151,6 @@ jobs:

cli-compat:
name: old-cli compat
if: github.event_name != 'pull_request'
runs-on: macos-latest
steps:
- name: Checkout code
Expand Down
114 changes: 114 additions & 0 deletions test/contract/contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//go:build contract

package contract_test

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/openbootdotdev/openboot/internal/config"
"github.com/openbootdotdev/openboot/internal/snapshot"
)

type canonicalPackageEntry struct {
Name string `json:"name"`
Desc string `json:"desc"`
}

type canonicalRemoteConfig struct {
Username string `json:"username"`
Slug string `json:"slug"`
Name string `json:"name"`
Preset string `json:"preset"`
Packages []canonicalPackageEntry `json:"packages"`
Casks []canonicalPackageEntry `json:"casks"`
Taps []string `json:"taps"`
Npm []canonicalPackageEntry `json:"npm"`
DotfilesRepo string `json:"dotfiles_repo"`
PostInstall []string `json:"post_install"`
Shell *config.RemoteShellConfig `json:"shell"`
MacOSPrefs []config.RemoteMacOSPref `json:"macos_prefs"`
}

func TestRemoteConfigFixtureIsConsumedLosslessly(t *testing.T) {
data := readContractFixture(t, "config-v1.json")

var wire canonicalRemoteConfig
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
require.NoError(t, decoder.Decode(&wire), "contract fixture must use the canonical remote-config shape")
require.NotEmpty(t, wire.Packages, "fixture must exercise formulae")
require.NotEmpty(t, wire.Casks, "fixture must exercise casks")
require.NotEmpty(t, wire.Taps, "fixture must exercise taps")
require.NotEmpty(t, wire.Npm, "fixture must exercise npm packages")

got, err := config.UnmarshalRemoteConfigFlexible(data)
require.NoError(t, err)
require.NoError(t, got.Validate())

want := &config.RemoteConfig{
Username: wire.Username,
Slug: wire.Slug,
Name: wire.Name,
Preset: wire.Preset,
Packages: packageEntries(wire.Packages),
Casks: packageEntries(wire.Casks),
Taps: wire.Taps,
Npm: packageEntries(wire.Npm),
DotfilesRepo: wire.DotfilesRepo,
PostInstall: wire.PostInstall,
Shell: wire.Shell,
MacOSPrefs: wire.MacOSPrefs,
}
assert.Equal(t, want, got, "CLI decoding must not repair, move, or drop fields from the canonical fixture")
}

func TestSnapshotFixtureIsConsumedLosslessly(t *testing.T) {
data := readContractFixture(t, "snapshot-v1.json")

var wire struct {
Packages struct {
Formulae []string `json:"formulae"`
Casks []string `json:"casks"`
Taps []string `json:"taps"`
Npm []string `json:"npm"`
} `json:"packages"`
}
require.NoError(t, json.Unmarshal(data, &wire))
require.NotEmpty(t, wire.Packages.Formulae, "fixture must exercise formulae")
require.NotEmpty(t, wire.Packages.Casks, "fixture must exercise casks")
require.NotEmpty(t, wire.Packages.Taps, "fixture must exercise taps")
require.NotEmpty(t, wire.Packages.Npm, "fixture must exercise npm packages")

got, err := snapshot.ParseBytes(data)
require.NoError(t, err)
assert.Equal(t, wire.Packages.Formulae, got.Packages.Formulae)
assert.Equal(t, wire.Packages.Casks, got.Packages.Casks)
assert.Equal(t, wire.Packages.Taps, got.Packages.Taps)
assert.Equal(t, wire.Packages.Npm, got.Packages.Npm)
}

func readContractFixture(t *testing.T, name string) []byte {
t.Helper()

contractDir := os.Getenv("OPENBOOT_CONTRACT_DIR")
require.NotEmpty(t, contractDir, "OPENBOOT_CONTRACT_DIR must point to an openboot-contract checkout")

data, err := os.ReadFile(filepath.Join(contractDir, "fixtures", name))
require.NoError(t, err)
return data
}

func packageEntries(entries []canonicalPackageEntry) config.PackageEntryList {
result := make(config.PackageEntryList, len(entries))
for i, entry := range entries {
result[i] = config.PackageEntry{Name: entry.Name, Desc: entry.Desc}
}
return result
}
Loading