{"contents":"package githttp\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\tgogit \"github.com/go-git/go-git/v5\"\n\t\"github.com/go-git/go-git/v5/plumbing\"\n\t\"github.com/go-git/go-git/v5/plumbing/object\"\n\t\"log/slog\"\n\n\tknotconfig \"tangled.org/http-knot/config\"\n\ts3store \"tangled.org/http-knot/storage/s3\"\n)\n\nfunc testS3Config() knotconfig.S3 {\n\treturn knotconfig.S3{\n\t\tEndpoint: envOr(\"KNOT_S3_ENDPOINT\", \"localhost:9000\"),\n\t\tBucket: envOr(\"KNOT_S3_BUCKET\", \"test-knot\"),\n\t\tRegion: envOr(\"KNOT_S3_REGION\", \"us-east-1\"),\n\t\tAccessKey: envOr(\"KNOT_S3_ACCESS_KEY\", \"minioadmin\"),\n\t\tSecretKey: envOr(\"KNOT_S3_SECRET_KEY\", \"minioadmin\"),\n\t\tUseSSL: false,\n\t}\n}\n\nfunc envOr(key, fallback string) string {\n\tif v := os.Getenv(key); v != \"\" {\n\t\treturn v\n\t}\n\treturn fallback\n}\n\nfunc skipIfNoS3(t *testing.T) knotconfig.S3 {\n\tt.Helper()\n\tcfg := testS3Config()\n\tc, err := s3store.NewClient(cfg, \"test\", \"connectivity\")\n\tif err != nil {\n\t\tt.Skipf(\"S3 not available: %v\", err)\n\t}\n\t_ = c\n\treturn cfg\n}\n\n// seedRepo creates a test repo in S3 with a single commit.\nfunc seedRepo(t *testing.T, cfg knotconfig.S3, did, name string) {\n\tt.Helper()\n\ts, err := s3store.NewStorage(cfg, did, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\thead := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(\"main\"))\n\tif err := s.SetReference(head); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tblob := s.NewEncodedObject()\n\tblob.SetType(plumbing.BlobObject)\n\tw, _ := blob.Writer()\n\tcontent := []byte(\"hello from http-knot\\n\")\n\tw.Write(content)\n\tw.Close()\n\tblob.SetSize(int64(len(content)))\n\tblobHash, err := s.SetEncodedObject(blob)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttreeObj := s.NewEncodedObject()\n\ttreeObj.SetType(plumbing.TreeObject)\n\ttree := object.Tree{\n\t\tEntries: []object.TreeEntry{\n\t\t\t{Name: \"README.md\", Mode: 0100644, Hash: blobHash},\n\t\t},\n\t}\n\tif err := tree.Encode(treeObj); err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttreeHash, err := s.SetEncodedObject(treeObj)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcommitObj := s.NewEncodedObject()\n\tcommitObj.SetType(plumbing.CommitObject)\n\tcommit := object.Commit{\n\t\tAuthor: object.Signature{Name: \"Test\", Email: \"test@test.com\", When: time.Now()},\n\t\tCommitter: object.Signature{Name: \"Test\", Email: \"test@test.com\", When: time.Now()},\n\t\tMessage: \"initial commit\\n\",\n\t\tTreeHash: treeHash,\n\t}\n\tif err := commit.Encode(commitObj); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcommitHash, err := s.SetEncodedObject(commitObj)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tmainRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName(\"main\"), commitHash)\n\tif err := s.SetReference(mainRef); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Verify\n\trepo, err := gogit.Open(s, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\th, err := repo.Head()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Logf(\"seeded repo %s/%s at %s\", did, name, h.Hash())\n}\n\nfunc testRouter(cfg knotconfig.S3) http.Handler {\n\tlogger := slog.New(slog.NewTextHandler(io.Discard, nil))\n\tfullCfg := \u0026knotconfig.Config{S3: cfg}\n\n\thandler := \u0026Handler{\n\t\ts3cfg: cfg,\n\t\tcfg: fullCfg,\n\t\tlogger: logger,\n\t}\n\n\tr := chi.NewRouter()\n\tr.Route(\"/{did}\", func(r chi.Router) {\n\t\tr.Route(\"/{name}\", func(r chi.Router) {\n\t\t\tr.Get(\"/info/refs\", handler.InfoRefs)\n\t\t\tr.Post(\"/git-upload-pack\", handler.UploadPack)\n\t\t\tr.Post(\"/git-receive-pack\", handler.ReceivePack)\n\t\t})\n\t})\n\treturn r\n}\n\nfunc TestInfoRefs(t *testing.T) {\n\tcfg := skipIfNoS3(t)\n\tdid := \"did:plc:test\"\n\tname := \"inforefs-test\"\n\tseedRepo(t, cfg, did, name)\n\n\tsrv := httptest.NewServer(testRouter(cfg))\n\tdefer srv.Close()\n\n\turl := fmt.Sprintf(\"%s/%s/%s/info/refs?service=git-upload-pack\", srv.URL, did, name)\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != 200 {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\tt.Fatalf(\"expected 200, got %d: %s\", resp.StatusCode, body)\n\t}\n\n\tct := resp.Header.Get(\"Content-Type\")\n\tif ct != \"application/x-git-upload-pack-advertisement\" {\n\t\tt.Fatalf(\"wrong content-type: %s\", ct)\n\t}\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tif len(body) == 0 {\n\t\tt.Fatal(\"empty response body\")\n\t}\n\tt.Logf(\"info/refs response: %d bytes\", len(body))\n}\n\nfunc TestGitClone(t *testing.T) {\n\tcfg := skipIfNoS3(t)\n\n\t// Check git is available\n\tif _, err := exec.LookPath(\"git\"); err != nil {\n\t\tt.Skip(\"git not in PATH\")\n\t}\n\n\tdid := \"did:plc:test\"\n\tname := \"clone-test\"\n\tseedRepo(t, cfg, did, name)\n\n\tsrv := httptest.NewServer(testRouter(cfg))\n\tdefer srv.Close()\n\n\ttmpDir := t.TempDir()\n\tcloneURL := fmt.Sprintf(\"%s/%s/%s\", srv.URL, did, name)\n\tcloneDst := tmpDir + \"/repo\"\n\n\tcmd := exec.Command(\"git\", \"clone\", cloneURL, cloneDst)\n\tout, err := cmd.CombinedOutput()\n\tt.Logf(\"git clone output: %s\", strings.TrimSpace(string(out)))\n\tif err != nil {\n\t\tt.Fatalf(\"git clone failed: %v\", err)\n\t}\n\n\t// Verify cloned content\n\tdata, err := os.ReadFile(cloneDst + \"/README.md\")\n\tif err != nil {\n\t\tt.Fatalf(\"read README: %v\", err)\n\t}\n\tif string(data) != \"hello from http-knot\\n\" {\n\t\tt.Fatalf(\"unexpected content: %q\", string(data))\n\t}\n\tt.Logf(\"cloned README.md: %q\", strings.TrimSpace(string(data)))\n}\n\nfunc TestGitPush(t *testing.T) {\n\tcfg := skipIfNoS3(t)\n\n\tif _, err := exec.LookPath(\"git\"); err != nil {\n\t\tt.Skip(\"git not in PATH\")\n\t}\n\n\tdid := \"did:plc:test\"\n\tname := \"push-test\"\n\tseedRepo(t, cfg, did, name)\n\n\tsrv := httptest.NewServer(testRouter(cfg))\n\tdefer srv.Close()\n\n\t// Clone first\n\ttmpDir := t.TempDir()\n\tcloneURL := fmt.Sprintf(\"%s/%s/%s\", srv.URL, did, name)\n\tcloneDst := tmpDir + \"/repo\"\n\n\tcmd := exec.Command(\"git\", \"clone\", cloneURL, cloneDst)\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tt.Fatalf(\"git clone failed: %v\\n%s\", err, out)\n\t}\n\n\t// Make a change\n\tif err := os.WriteFile(cloneDst+\"/newfile.txt\", []byte(\"new content\\n\"), 0644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Git add \u0026 commit\n\tcmd = exec.Command(\"git\", \"-C\", cloneDst, \"add\", \"newfile.txt\")\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tt.Fatalf(\"git add: %v\\n%s\", err, out)\n\t}\n\n\tcmd = exec.Command(\"git\", \"-C\", cloneDst, \"commit\", \"-m\", \"add newfile\")\n\tcmd.Env = append(os.Environ(),\n\t\t\"GIT_AUTHOR_NAME=Test\",\n\t\t\"GIT_AUTHOR_EMAIL=test@test.com\",\n\t\t\"GIT_COMMITTER_NAME=Test\",\n\t\t\"GIT_COMMITTER_EMAIL=test@test.com\",\n\t)\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tt.Fatalf(\"git commit: %v\\n%s\", err, out)\n\t}\n\n\t// Push\n\tcmd = exec.Command(\"git\", \"-C\", cloneDst, \"push\", \"origin\", \"main\")\n\tout, err = cmd.CombinedOutput()\n\tt.Logf(\"git push output: %s\", strings.TrimSpace(string(out)))\n\tif err != nil {\n\t\tt.Fatalf(\"git push failed: %v\", err)\n\t}\n\n\t// Verify the pushed content is in S3\n\ts, err := s3store.NewStorage(cfg, did, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trepo, err := gogit.Open(s, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tref, err := repo.Reference(plumbing.NewBranchReferenceName(\"main\"), true)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tc, err := repo.CommitObject(ref.Hash())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !strings.Contains(c.Message, \"add newfile\") {\n\t\tt.Fatalf(\"expected pushed commit, got: %q\", c.Message)\n\t}\n\tt.Logf(\"pushed commit verified: %s %s\", c.Hash, strings.TrimSpace(c.Message))\n}\n","path":"githttp/handler_test.go","ref":"main"}