352 lines
15 KiB
Go
352 lines
15 KiB
Go
package workspaceops
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"context"
|
|
"github.com/aritmolab/thothii/tools/thothctl/internal/compose"
|
|
"github.com/aritmolab/thothii/tools/thothctl/internal/config"
|
|
)
|
|
|
|
func TestParseWorkspaceCommands(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
argv []string
|
|
want any
|
|
}{
|
|
{"inspect", []string{"workspace", "inspect", "--workspace", "psd", "--json"}, InspectCommand{WorkspaceID: "psd", JSON: true}},
|
|
{"dwh", []string{"workspace", "preprocess", "dwh", "--workspace", "psd", "--resume", "0123456789abcdef0123456789abcdef"}, DwhRequest{WorkspaceID: "psd", Resume: "0123456789abcdef0123456789abcdef"}},
|
|
{"suggest", []string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", "a=b"}, SuggestFksRequest{WorkspaceID: "psd", Assume: []string{"a=b"}}},
|
|
{"check", []string{"workspace", "schema", "check", "--workspace", "psd", "--resume", "0123456789abcdef0123456789abcdef"}, CheckSchemaRequest{WorkspaceID: "psd", Resume: "0123456789abcdef0123456789abcdef"}},
|
|
{"index", []string{"workspace", "index-schema", "--workspace", "psd"}, IndexSchemaRequest{WorkspaceID: "psd"}},
|
|
{"evidence", []string{"workspace", "preprocess", "evidence", "--workspace", "psd", "--dry-run"}, EvidenceRequest{WorkspaceID: "psd", DryRun: true}},
|
|
{"run", []string{"workspace", "preprocess", "run", "--workspace", "psd"}, RunRequest{WorkspaceID: "psd"}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := ParseWorkspaceCommand(tc.argv)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Errorf("got %#v want %#v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseWorkspaceRejectsUnsafeOrAmbiguousOptions(t *testing.T) {
|
|
bad := [][]string{
|
|
{"workspace", "inspect", "--workspace", "psd", "--resume", "0123456789abcdef0123456789abcdef"},
|
|
{"workspace", "inspect", "--workspace", "psd", "--bootstrap-run-id", "0123456789abcdef0123456789abcdef"},
|
|
{"workspace", "inspect", "--workspace", "psd", "--passthrough"},
|
|
{"workspace", "preprocess", "run", "--workspace", "PSd"},
|
|
{"workspace", "preprocess", "run", "--workspace", "psd", "--resume", "bad"},
|
|
{"workspace", "schema", "check", "--workspace", "psd"},
|
|
{"workspace", "schema", "check", "--workspace", "psd", "--resume", "0123456789abcdef0123456789abcdef", "--annotations", "a"},
|
|
}
|
|
for _, argv := range bad {
|
|
if _, err := ParseWorkspaceCommand(argv); err == nil {
|
|
t.Errorf("accepted unsafe argv %v", argv)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseWorkspaceRejectsNonContractHierarchyAndAssumeShell(t *testing.T) {
|
|
for _, argv := range [][]string{
|
|
{"workspace", "schema", "dwh", "--workspace", "psd"},
|
|
{"workspace", "preprocess", "check", "--workspace", "psd"},
|
|
{"workspace", "suggest-fks", "--workspace", "psd"},
|
|
{"workspace", "inspect", "--workspace", "psd", "--bootstrap-run-id", strings.Repeat("a", 32)},
|
|
{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", "a"},
|
|
{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", "a=$(id)"},
|
|
} {
|
|
if _, err := ParseWorkspaceCommand(argv); err == nil {
|
|
t.Errorf("accepted non-contract argv %v", argv)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunUsesBase64BasenameIngressAndNoTTY(t *testing.T) {
|
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sqlPath := filepath.Join(root, "schema.sql")
|
|
if err := os.WriteFile(sqlPath, []byte("select 1"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stdinCapture := filepath.Join(root, "stdin.json")
|
|
argsCapture := filepath.Join(root, "args.txt")
|
|
resultJSON := `{"schemaVersion":1,"status":"succeeded","code":"ok","workspaceId":"psd","workspaceRevision":"0123456789012345678901234567890123456789","descriptorBlob":"abcdefabcdefabcdefabcdefabcdefabcdefabcd","operation":"suggest-fks","runId":"0123456789abcdef0123456789abcdef","completedStages":[]}`
|
|
script := "#!/bin/sh\nprintf '%s\n' \"$@\" > '" + argsCapture + "'\ncat > '" + stdinCapture + "'\nprintf '%s' '" + resultJSON + "'\n"
|
|
fake := filepath.Join(root, "docker")
|
|
if err := os.WriteFile(fake, []byte(script), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--from-sql", sqlPath})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := Run(context.Background(), config.Installation{ProjectDirectory: root, EnvFile: filepath.Join(root, "env")}, compose.NewRunner(fake), cmd, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Code != "ok" {
|
|
t.Fatalf("result = %#v", got)
|
|
}
|
|
stdinBytes, err := os.ReadFile(stdinCapture)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(stdinBytes), sqlPath) || !strings.Contains(string(stdinBytes), "contentBase64") || !strings.Contains(string(stdinBytes), "schema.sql") {
|
|
t.Fatalf("stdin envelope = %q; want basename/base64 without host path", stdinBytes)
|
|
}
|
|
argsBytes, err := os.ReadFile(argsCapture)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(argsBytes), sqlPath) || !strings.Contains(string(argsBytes), "--operation\nsuggest-fks") || !strings.Contains(string(argsBytes), "--workspace\npsd") {
|
|
t.Fatalf("child args = %q; want closed operation/workspace args", argsBytes)
|
|
}
|
|
}
|
|
|
|
func TestRunPublishesOnlyVerifiedCandidateExport(t *testing.T) {
|
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
candidate := []byte("candidates: []\n")
|
|
digest := DigestBytes(candidate)
|
|
encoded := base64.StdEncoding.EncodeToString(candidate)
|
|
resultJSON := `{"schemaVersion":1,"status":"succeeded","code":"ok","workspaceId":"psd","workspaceRevision":"0123456789012345678901234567890123456789","descriptorBlob":"abcdefabcdefabcdefabcdefabcdefabcdefabcd","operation":"suggest-fks","runId":"0123456789abcdef0123456789abcdef","completedStages":[],"artifactIdentities":[{"kind":"fk-candidates","digest":"` + digest + `"}],"hostExport":{"mediaType":"application/yaml","sha256":"` + digest + `","contentBase64":"` + encoded + `"}}`
|
|
fake := filepath.Join(root, "docker")
|
|
if err := os.WriteFile(fake, []byte("#!/bin/sh\ncat >/dev/null\nprintf '%s' '"+resultJSON+"'\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(root, "out.yaml")
|
|
cmd, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--output", path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := Run(context.Background(), config.Installation{ProjectDirectory: root, EnvFile: filepath.Join(root, "env")}, compose.NewRunner(fake), cmd, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.RunID == "" {
|
|
t.Fatal("missing run identity")
|
|
}
|
|
b, err := os.ReadFile(path)
|
|
if err != nil || string(b) != string(candidate) {
|
|
t.Fatalf("candidate = %q, %v", b, err)
|
|
}
|
|
}
|
|
|
|
func validWorkspaceResult(status, code string) Result {
|
|
return Result{SchemaVersion: 1, Status: status, Code: code, WorkspaceID: "psd", WorkspaceRevision: strings.Repeat("0", 40), DescriptorBlob: strings.Repeat("a", 40), Operation: "evidence", CompletedStages: []string{}}
|
|
}
|
|
|
|
func TestValidateIngressRejectsExplicitOmittedEvidenceZeroFields(t *testing.T) {
|
|
command := EvidenceRequest{WorkspaceID: "psd"}
|
|
expected, _, err := makeInput(command)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, payload := range []string{
|
|
`{"schemaVersion":1,"operation":"evidence","workspaceId":"psd","resume":""}`,
|
|
`{"schemaVersion":1,"operation":"evidence","workspaceId":"psd","workspaceId":"psd"}`,
|
|
`{"schemaVersion":1,"operation":"evidence","workspaceId":"psd","dryRun":false}`,
|
|
`{"schemaVersion":1,"operation":"evidence","workspaceId":"psd","resume":null}`,
|
|
} {
|
|
if err := validateIngress([]byte(payload), expected); err == nil {
|
|
t.Errorf("accepted non-derived envelope %s", payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateIngressRequiresExactDerivedFieldSetForOptionalInputs(t *testing.T) {
|
|
command := SuggestFksRequest{WorkspaceID: "psd"}
|
|
expected, _, err := makeInput(command)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
payload := `{"schemaVersion":1,"operation":"suggest-fks","workspaceId":"psd","sql":null,"assume":[]}`
|
|
if err := validateIngress([]byte(payload), expected); err == nil {
|
|
t.Fatal("accepted explicit null/empty optional fields omitted by command")
|
|
}
|
|
}
|
|
|
|
func TestPublishCandidateValidatesInternalExportWithoutOutput(t *testing.T) {
|
|
candidate := []byte("candidates: []\n")
|
|
digest := DigestBytes(candidate)
|
|
result := validWorkspaceResult("succeeded", "ok")
|
|
result.Operation = "suggest-fks"
|
|
result.RunID = strings.Repeat("b", 32)
|
|
result.ArtifactIdentities = []ArtifactIdentity{{Kind: "fk-candidates", Digest: digest}}
|
|
for _, export := range []hostExport{
|
|
{MediaType: "application/json", SHA256: digest, ContentBase64: base64.StdEncoding.EncodeToString(candidate)},
|
|
{MediaType: "application/yaml", SHA256: digest, ContentBase64: base64.StdEncoding.EncodeToString(candidate)},
|
|
} {
|
|
if export.MediaType == "application/json" {
|
|
if err := publishCandidate(&export, result, ""); err == nil {
|
|
t.Fatal("accepted invalid media type without output")
|
|
}
|
|
} else if err := publishCandidate(&export, result, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateResultBindsStatusCodeAndExit(t *testing.T) {
|
|
cases := []struct {
|
|
status, code string
|
|
exit int
|
|
valid bool
|
|
}{
|
|
{"succeeded", "ok", 0, true}, {"unchanged", "ok", 0, true}, {"dry_run", "ok", 0, true},
|
|
{"blocked", "manual_review_required", 3, true}, {"failed", "workspace_not_found", 1, true},
|
|
{"blocked", "manual_review_required", 0, false}, {"failed", "workspace_not_found", 0, false},
|
|
{"succeeded", "workspace_not_found", 0, false}, {"failed", "ok", 1, false},
|
|
}
|
|
for _, tc := range cases {
|
|
r := validWorkspaceResult(tc.status, tc.code)
|
|
err := validateResult(r, "psd", "evidence")
|
|
got := err == nil && resultExitMatches(tc.status, tc.exit)
|
|
if got != tc.valid {
|
|
t.Errorf("%s/%s exit %d valid=%v err=%v", tc.status, tc.code, tc.exit, tc.valid, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunRejectsUnknownHostExportFieldWithoutOutput(t *testing.T) {
|
|
d := t.TempDir()
|
|
candidate := []byte("candidates: []\n")
|
|
digest := DigestBytes(candidate)
|
|
response := `{"schemaVersion":1,"status":"succeeded","code":"ok","workspaceId":"psd","workspaceRevision":"` + strings.Repeat("0", 40) + `","descriptorBlob":"` + strings.Repeat("a", 40) + `","operation":"suggest-fks","runId":"` + strings.Repeat("b", 32) + `","completedStages":[],"artifactIdentities":[{"kind":"fk-candidates","digest":"` + digest + `"}],"hostExport":{"mediaType":"application/yaml","sha256":"` + digest + `","contentBase64":"` + base64.StdEncoding.EncodeToString(candidate) + `","extra":1}}`
|
|
fake := filepath.Join(d, "docker")
|
|
if err := os.WriteFile(fake, []byte("#!/bin/sh\ncat >/dev/null\nprintf '%s' '"+response+"'\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd := SuggestFksRequest{WorkspaceID: "psd"}
|
|
if _, err := Run(context.Background(), config.Installation{ProjectDirectory: d}, compose.NewRunner(fake), cmd, nil); err == nil {
|
|
t.Fatal("accepted unknown hostExport field")
|
|
}
|
|
}
|
|
|
|
func TestPublishCandidateRejectsOversizedExportWithoutOutput(t *testing.T) {
|
|
candidate := bytes.Repeat([]byte("y"), maxCandidate+1)
|
|
digest := DigestBytes(candidate)
|
|
x := hostExport{MediaType: "application/yaml", SHA256: digest, ContentBase64: base64.StdEncoding.EncodeToString(candidate)}
|
|
r := validWorkspaceResult("succeeded", "ok")
|
|
r.Operation, r.RunID = "suggest-fks", strings.Repeat("b", 32)
|
|
if err := publishCandidate(&x, r, ""); err == nil {
|
|
t.Fatal("accepted oversized export without output")
|
|
}
|
|
}
|
|
|
|
func TestRunAcceptsOnlyMatchingChildExitForStatus(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, status, code string
|
|
exit int
|
|
wantErr bool
|
|
}{
|
|
{"blocked-exit3", "blocked", "manual_review_required", 3, false},
|
|
{"blocked-exit0", "blocked", "manual_review_required", 0, true},
|
|
{"failed-exit1", "failed", "workspace_not_found", 1, false},
|
|
{"failed-exit0", "failed", "workspace_not_found", 0, true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
d := t.TempDir()
|
|
response := fmt.Sprintf(`{"schemaVersion":1,"status":"%s","code":"%s","workspaceId":"psd","workspaceRevision":"%s","descriptorBlob":"%s","operation":"evidence","completedStages":[]}`,
|
|
tc.status, tc.code, strings.Repeat("0", 40), strings.Repeat("a", 40))
|
|
fake := filepath.Join(d, "docker")
|
|
if err := os.WriteFile(fake, []byte("#!/bin/sh\ncat >/dev/null\nprintf '%s' '"+response+"'\nexit "+fmt.Sprint(tc.exit)+"\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := Run(context.Background(), config.Installation{ProjectDirectory: d}, compose.NewRunner(fake), EvidenceRequest{WorkspaceID: "psd"}, nil)
|
|
if (err != nil) != tc.wantErr {
|
|
t.Fatalf("error=%v wantErr=%v", err, tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseWorkspaceRejectsDuplicateMissingAndUnknownOptions(t *testing.T) {
|
|
validResume := strings.Repeat("0", 32)
|
|
for _, argv := range [][]string{
|
|
{"workspace", "inspect", "--json", "--json", "--workspace", "psd"},
|
|
{"workspace", "inspect", "--workspace", "psd", "--workspace", "psd"},
|
|
{"workspace", "preprocess", "dwh", "--workspace", "psd", "--resume", validResume, "--resume", validResume},
|
|
{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--output", "a", "--output", "b"},
|
|
{"workspace", "inspect", "--json", "--unknown", "x", "--workspace", "psd"},
|
|
{"workspace", "inspect", "--json"},
|
|
} {
|
|
if _, err := ParseWorkspaceCommand(argv); err == nil {
|
|
t.Errorf("accepted invalid argv %v", argv)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseWorkspaceAcceptsAssumptionAndEnforcesLimits(t *testing.T) {
|
|
if _, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", "orders.id=customers.id"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", strings.Repeat("a", maxAssumptionBytes+1)}); err == nil {
|
|
t.Fatal("accepted oversized assumption")
|
|
}
|
|
args := []string{"workspace", "schema", "suggest-fks", "--workspace", "psd"}
|
|
for i := 0; i < 33; i++ {
|
|
args = append(args, "--from-sql", fmt.Sprintf("schema-%d.sql", i))
|
|
}
|
|
if _, err := ParseWorkspaceCommand(args); err == nil {
|
|
t.Fatal("accepted 33 SQL files")
|
|
}
|
|
}
|
|
|
|
func TestMakeInputAcceptsMaximumSingleSQLFile(t *testing.T) {
|
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(root, "schema.sql")
|
|
if err := os.WriteFile(path, bytes.Repeat([]byte("x"), maxSQLFile), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
command := SuggestFksRequest{WorkspaceID: "psd", FromSQL: []string{path}}
|
|
if _, payload, err := makeInput(command); err != nil {
|
|
t.Fatalf("makeInput() error = %v", err)
|
|
} else if len(payload) <= maxResult {
|
|
t.Fatalf("payload length = %d, want larger than result cap %d", len(payload), maxResult)
|
|
}
|
|
}
|
|
|
|
func TestMakeInputAcceptsMaximumAnnotationWithReviewDigestPair(t *testing.T) {
|
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(root, "annotations.md")
|
|
contents := bytes.Repeat([]byte("a"), maxAnnotations)
|
|
if err := os.WriteFile(path, contents, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
command := CheckSchemaRequest{WorkspaceID: "psd", Resume: strings.Repeat("0", 32), Annotations: path, ReviewedCandidates: DigestBytes([]byte("reviewed"))}
|
|
env, payload, err := makeInput(command)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if env.Annotations == nil || env.ReviewedCandidates == "" || len(payload) <= maxResult {
|
|
t.Fatalf("annotation envelope = %#v payload=%d; want pair and request larger than result cap", env, len(payload))
|
|
}
|
|
if err := validateIngress([]byte(payload), env); err != nil {
|
|
t.Fatalf("validateIngress() = %v", err)
|
|
}
|
|
}
|