Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ updates:
- /modules/dynamodb
- /modules/elasticsearch
- /modules/etcd
- /modules/forgejo
- /modules/gcloud
- /modules/grafana-lgtm
- /modules/inbucket
Expand Down
4 changes: 4 additions & 0 deletions .vscode/.testcontainers-go.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@
"name": "module / etcd",
"path": "../modules/etcd"
},
{
"name": "module / forgejo",
"path": "../modules/forgejo"
},
{
"name": "module / gcloud",
"path": "../modules/gcloud"
Expand Down
76 changes: 76 additions & 0 deletions docs/modules/forgejo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Forgejo

Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

## Introduction

The Testcontainers module for [Forgejo](https://forgejo.org/), a self-hosted Git forge. Forgejo is a community-driven fork of Gitea, providing a lightweight code hosting solution.

## Adding this module to your project dependencies

Please run the following command to add the Forgejo module to your Go dependencies:

```sh
go get github.com/testcontainers/testcontainers-go/modules/forgejo
```

## Usage example

<!--codeinclude-->
[Creating a Forgejo container](../../modules/forgejo/examples_test.go) inside_block:runForgejoContainer
<!--/codeinclude-->

## Module Reference

### Run function

- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

The Forgejo module exposes one entrypoint function to create the Forgejo container, and this function receives three parameters:

```golang
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error)
```

- `context.Context`, the Go context.
- `string`, the Docker image to use.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

#### Image

Use the second argument in the `Run` function to set a valid Docker image.
In example: `Run(context.Background(), "codeberg.org/forgejo/forgejo:11")`.

### Container Options

When starting the Forgejo container, you can pass options in a variadic way to configure it.

#### Admin Credentials

- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

Use `WithAdminCredentials(username, password, email)` to set the admin user credentials. An admin user is automatically created when the container starts. Default credentials are `forgejo-admin` / `forgejo-admin`.

#### Configuration via Environment

- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

Use `WithConfig(section, key, value)` to set Forgejo configuration values using the `FORGEJO__section__key` environment variable format. See the [Forgejo Configuration Cheat Sheet](https://forgejo.org/docs/latest/admin/config-cheat-sheet/) for available options.

{% include "../features/common_functional_options_list.md" %}

### Container Methods

The Forgejo container exposes the following methods:

#### ConnectionString

- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

The `ConnectionString` method returns the HTTP URL for the Forgejo instance (e.g. `http://localhost:12345`).

#### SSHConnectionString

- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

The `SSHConnectionString` method returns the SSH endpoint for Git operations.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ nav:
- modules/dynamodb.md
- modules/elasticsearch.md
- modules/etcd.md
- modules/forgejo.md
- modules/gcloud.md
- modules/grafana-lgtm.md
- modules/inbucket.md
Expand Down
5 changes: 5 additions & 0 deletions modules/forgejo/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include ../../commons-test.mk

.PHONY: test
test:
$(MAKE) test-forgejo
38 changes: 38 additions & 0 deletions modules/forgejo/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package forgejo_test

import (
"context"
"fmt"
"log"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/forgejo"
)

func ExampleRun() {
// runForgejoContainer {
ctx := context.Background()

forgejoContainer, err := forgejo.Run(ctx, "codeberg.org/forgejo/forgejo:11")
defer func() {
if err := testcontainers.TerminateContainer(forgejoContainer); err != nil {
log.Printf("failed to terminate container: %s", err)
}
}()
if err != nil {
log.Printf("failed to start container: %s", err)
return
}
// }

state, err := forgejoContainer.State(ctx)
if err != nil {
log.Printf("failed to get container state: %s", err)
return
}

fmt.Println(state.Running)

// Output:
// true
}
165 changes: 165 additions & 0 deletions modules/forgejo/forgejo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package forgejo

import (
"context"
"fmt"
"io"
"strings"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/wait"
)

const (
defaultHTTPPort = "3000/tcp"
defaultSSHPort = "22/tcp"
defaultUser = "forgejo-admin"
defaultPassword = "forgejo-admin"
defaultEmail = "admin@forgejo.local"
)

// Container represents the Forgejo container type used in the module
type Container struct {
testcontainers.Container
AdminUsername string
AdminPassword string
Comment thread
mdelapenya marked this conversation as resolved.
Outdated
}

// extractAdminCredentials parses FORGEJO_ADMIN_* env vars from the container
// environment, falling back to the default values for any that are not set.
func extractAdminCredentials(env []string) (username, password, email string) {
username, password, email = defaultUser, defaultPassword, defaultEmail
for _, e := range env {
if v, ok := strings.CutPrefix(e, "FORGEJO_ADMIN_USERNAME="); ok {
username = v
}
if v, ok := strings.CutPrefix(e, "FORGEJO_ADMIN_PASSWORD="); ok {
password = v
}
if v, ok := strings.CutPrefix(e, "FORGEJO_ADMIN_EMAIL="); ok {
email = v
}
}
return
}

// Run creates an instance of the Forgejo container type
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
// Closure variables populated by the PostReadies hook so we can avoid
// a second container.Inspect call after Run returns.
var adminUser, adminPass string

moduleOpts := []testcontainers.ContainerCustomizer{
testcontainers.WithExposedPorts(defaultHTTPPort, defaultSSHPort),
testcontainers.WithWaitStrategy(
wait.ForHTTP("/api/healthz").WithPort("3000"),
),
Comment thread
mdelapenya marked this conversation as resolved.
// Use SQLite for simplicity in tests (no external DB needed).
// INSTALL_LOCK skips the install wizard so the instance is ready to use.
testcontainers.WithEnv(map[string]string{
"FORGEJO__database__DB_TYPE": "sqlite3",
"FORGEJO__security__INSTALL_LOCK": "true",
"FORGEJO_ADMIN_USERNAME": defaultUser,
"FORGEJO_ADMIN_PASSWORD": defaultPassword,
"FORGEJO_ADMIN_EMAIL": defaultEmail,
}),
}

moduleOpts = append(moduleOpts, opts...)

// Add lifecycle hook to create admin user after container is ready.
// The hook reads credentials from container env vars so that user-provided
// options (which override the defaults above) are respected.
// The command runs as the "git" user because Forgejo refuses to run CLI
// commands as root.
adminHook := testcontainers.ContainerLifecycleHooks{
PostReadies: []testcontainers.ContainerHook{
func(ctx context.Context, container testcontainers.Container) error {
inspect, err := container.Inspect(ctx)
if err != nil {
return fmt.Errorf("inspect forgejo: %w", err)
}

username, password, email := extractAdminCredentials(inspect.Config.Env)

// Store credentials in closure for Run to use later.
adminUser = username
adminPass = password

code, output, err := container.Exec(ctx, []string{
"forgejo", "admin", "user", "create",
"--username", username,
"--password", password,
"--email", email,
"--admin",
"--must-change-password=false",
}, exec.WithUser("git"))
if err != nil {
return fmt.Errorf("create admin user: %w", err)
}
if code != 0 {
data, _ := io.ReadAll(output)
return fmt.Errorf("create admin user: exit code %d: %s", code, string(data))
}
return nil
},
},
}

moduleOpts = append(moduleOpts, testcontainers.WithAdditionalLifecycleHooks(adminHook))

ctr, err := testcontainers.Run(ctx, img, moduleOpts...)
var c *Container
if ctr != nil {
c = &Container{Container: ctr}
}

if err != nil {
return c, fmt.Errorf("run forgejo: %w", err)
}

// Credentials were populated by the PostReadies hook above.
c.AdminUsername = adminUser
c.AdminPassword = adminPass

return c, nil
}

// ConnectionString returns the HTTP URL for the Forgejo instance
func (c *Container) ConnectionString(ctx context.Context) (string, error) {
return c.PortEndpoint(ctx, defaultHTTPPort, "http")
}

// SSHConnectionString returns the SSH endpoint for Git operations
func (c *Container) SSHConnectionString(ctx context.Context) (string, error) {
return c.PortEndpoint(ctx, defaultSSHPort, "")
}

// WithAdminCredentials sets the admin username, password, and email for the Forgejo instance.
// These credentials are used to create an admin user after the container is ready.
func WithAdminCredentials(username, password, email string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) error {
if req.Env == nil {
req.Env = make(map[string]string)
}
req.Env["FORGEJO_ADMIN_USERNAME"] = username
req.Env["FORGEJO_ADMIN_PASSWORD"] = password
req.Env["FORGEJO_ADMIN_EMAIL"] = email
return nil
}
}

// WithConfig sets a Forgejo configuration value using the FORGEJO__section__key
// environment variable format.
// See https://forgejo.org/docs/latest/admin/config-cheat-sheet/ for available options.
func WithConfig(section, key, value string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) error {
if req.Env == nil {
req.Env = make(map[string]string)
}
envKey := fmt.Sprintf("FORGEJO__%s__%s", section, key)
req.Env[envKey] = value
return nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
81 changes: 81 additions & 0 deletions modules/forgejo/forgejo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package forgejo_test

import (
"context"
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/forgejo"
)

func TestForgejo(t *testing.T) {
ctx := context.Background()

ctr, err := forgejo.Run(ctx, "codeberg.org/forgejo/forgejo:11")
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)

// verify connection string returns a valid HTTP URL
connStr, err := ctr.ConnectionString(ctx)
require.NoError(t, err)
require.NotEmpty(t, connStr)

// verify the health endpoint is reachable via the connection string
req, err := http.NewRequestWithContext(ctx, http.MethodGet, connStr+"/api/healthz", nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)

// verify default admin credentials are set
require.Equal(t, "forgejo-admin", ctr.AdminUsername)
require.Equal(t, "forgejo-admin", ctr.AdminPassword)
}

func TestForgejoWithAdminCredentials(t *testing.T) {
ctx := context.Background()

ctr, err := forgejo.Run(ctx,
"codeberg.org/forgejo/forgejo:11",
forgejo.WithAdminCredentials("testuser", "testpassword", "test@example.com"),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)

// verify custom admin credentials are set on the container struct
require.Equal(t, "testuser", ctr.AdminUsername)
require.Equal(t, "testpassword", ctr.AdminPassword)

// verify the API is reachable and admin user can authenticate
connStr, err := ctr.ConnectionString(ctx)
require.NoError(t, err)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, connStr+"/api/v1/user", nil)
require.NoError(t, err)
req.SetBasicAuth("testuser", "testpassword")

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
}

func TestForgejoSSHEndpoint(t *testing.T) {
ctx := context.Background()

ctr, err := forgejo.Run(ctx, "codeberg.org/forgejo/forgejo:11")
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)

sshStr, err := ctr.SSHConnectionString(ctx)
require.NoError(t, err)
require.NotEmpty(t, sshStr)

// verify the SSH connection string contains a host and port
require.True(t, strings.Contains(sshStr, ":"), "SSH connection string should contain host:port")
}
Loading