package mapping import ( "os" "os/exec" "path/filepath" "strings" "testing" ) // sdkPath is the one third-party import this package is allowed: the payload // shapes it translates to and from. const sdkPath = "code.gitea.io/sdk/gitea" // The bridge translates values and nothing else: no network, no filesystem, no // clock, no configuration. Every one of those is a caller's to supply, which is // what lets this package be reasoned about and tested without a Gitea anywhere. // // THE RULE THIS TEST USED TO MAKE was stronger and is no longer true. The // shapes lived in internal/wire, which imported the standard library and // nothing else, so "the bridge cannot reach a transport" held by construction: // there was nothing in its dependency graph that could open a socket. The SDK's // types come with the SDK's client attached, so the graph now contains an HTTP // client whatever this package does with it — and a test that claimed otherwise // would be a test that lies. // // So it asserts the part that survives, which is also the part that catches a // real mistake: what THIS package reaches for. DIRECT imports, not the // dependency walk internal/issue does — the domain reaches os through // internal/project and that is the domain's business. A transport that grew a // helper here, or a lookup that quietly opened a config file, is what this // catches, and it still fails on `os`, on `net/http` and on internal/gitea. func TestTheBridgeTranslatesAndNothingElse(t *testing.T) { forbidden := map[string]string{ "net/http": "an HTTP call belongs in the transport", "net": "an HTTP call belongs in the transport", "os": "a pure function reads no file and no environment", "os/exec": "nothing here shells out", "io/ioutil": "a pure function reads no file", "git.noodles.cam/claude-skills/marketplace/cli/internal/gitea": "the transport imports this package, never the reverse", "git.noodles.cam/claude-skills/marketplace/cli/internal/config": "credentials and repositories are the transport's", "git.noodles.cam/claude-skills/marketplace/cli/internal/project": "nothing here resolves a path", } allowed := map[string]bool{ sdkPath: true, "git.noodles.cam/claude-skills/marketplace/cli/internal/issue": true, "git.noodles.cam/claude-skills/marketplace/cli/internal/wire": true, } for _, dep := range directImports(t) { if why, bad := forbidden[dep]; bad { t.Errorf("mapping imports %s — %s", dep, why) continue } // A standard-library import path has no dot in its first element, // because it has no domain name in front of it. Everything else has to // be named above: one third party is a decision, two is a habit. first, _, _ := strings.Cut(dep, "/") if strings.Contains(first, ".") && !allowed[dep] { t.Errorf("mapping imports %s — the only payload vocabulary here is %s", dep, sdkPath) } } } // The clock is the caller's, and `time` alone can no longer say so: the SDK // hands over a time.Time, so this package imports the package to format one // back into the string an issue file holds. What it must never do is ASK what // time it is — a `synced:` stamped here would be stamped at translation rather // than at the write it describes, and two issues pushed in one run would carry // two different times for one run. // // The source, then, rather than the import graph: the difference between // formatting a timestamp and having a clock is not visible in `go list`. func TestTheBridgeHasNoClock(t *testing.T) { for _, path := range sourceFiles(t) { raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } for _, banned := range []string{"time.Now(", "time.Since(", "time.Until("} { if strings.Contains(string(raw), banned) { t.Errorf("%s calls %s) — the clock belongs to the caller", filepath.Base(path), banned) } } } } func directImports(t *testing.T) []string { t.Helper() out, err := exec.Command("go", "list", "-f", `{{join .Imports "\n"}}`, ".").Output() if err != nil { t.Fatalf("go list: %v", err) } return strings.Fields(string(out)) } // sourceFiles is this package's own .go files, tests excluded: a test may look // at a clock, and one of them does. func sourceFiles(t *testing.T) []string { t.Helper() out, err := exec.Command("go", "list", "-f", `{{range .GoFiles}}{{.}} {{end}}`, ".").Output() if err != nil { t.Fatalf("go list: %v", err) } var paths []string for _, name := range strings.Fields(string(out)) { paths = append(paths, name) } if len(paths) == 0 { t.Fatal("go list named no source files — this test would pass on an empty package") } return paths }