mirror of
https://github.com/Maronato/go-finger.git
synced 2025-03-15 00:34:47 +00:00
Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
|
4f05b73bec | ||
|
8673f0db42 | ||
|
de3da93523 | ||
|
e2ea9cd975 |
14 changed files with 999 additions and 855 deletions
106
Dockerfile
106
Dockerfile
|
@ -1,52 +1,54 @@
|
|||
# Load golang image
|
||||
FROM golang:1.21-alpine as builder
|
||||
|
||||
RUN apk add make
|
||||
|
||||
ARG VERSION=undefined
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Set our build environment
|
||||
ENV GOCACHE=/tmp/.go-build-cache
|
||||
# This variable communicates to the service that it's running inside
|
||||
# a docker container.
|
||||
ENV ENV_DOCKER=true
|
||||
|
||||
# Copy dockerignore files
|
||||
COPY .dockerignore ./
|
||||
|
||||
# Install go deps using the cache
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/tmp/.go-build-cache \
|
||||
go mod download -x
|
||||
|
||||
COPY Makefile ./
|
||||
|
||||
# Copy source files
|
||||
COPY main.go ./
|
||||
COPY cmd cmd
|
||||
COPY internal internal
|
||||
|
||||
# Build it
|
||||
RUN --mount=type=cache,target=/tmp/.go-build-cache \
|
||||
make build VERSION=$VERSION
|
||||
|
||||
# Now create a new image with just the binary
|
||||
FROM gcr.io/distroless/static-debian11:nonroot
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY urns.yml /app/urns.yml
|
||||
|
||||
# Set our runtime environment
|
||||
ENV ENV_DOCKER=true
|
||||
|
||||
COPY --from=builder /go/src/app/finger /usr/local/bin/finger
|
||||
|
||||
HEALTHCHECK CMD [ "finger", "healthcheck" ]
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT [ "finger" ]
|
||||
CMD [ "serve" ]
|
||||
# Load golang image
|
||||
FROM golang:1.21-alpine as builder
|
||||
|
||||
RUN apk add make
|
||||
|
||||
ARG VERSION=undefined
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Set our build environment
|
||||
ENV GOCACHE=/tmp/.go-build-cache
|
||||
# This variable communicates to the service that it's running inside
|
||||
# a docker container.
|
||||
ENV ENV_DOCKER=true
|
||||
|
||||
# Copy dockerignore files
|
||||
COPY .dockerignore ./
|
||||
|
||||
# Install go deps using the cache
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/tmp/.go-build-cache \
|
||||
go mod download -x
|
||||
|
||||
COPY Makefile ./
|
||||
|
||||
# Copy source files
|
||||
COPY main.go ./
|
||||
COPY cmd cmd
|
||||
COPY internal internal
|
||||
COPY webfingers webfingers
|
||||
COPY handler handler
|
||||
|
||||
# Build it
|
||||
RUN --mount=type=cache,target=/tmp/.go-build-cache \
|
||||
make build VERSION=$VERSION
|
||||
|
||||
# Now create a new image with just the binary
|
||||
FROM gcr.io/distroless/static-debian11:nonroot
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY urns.yml /app/urns.yml
|
||||
|
||||
# Set our runtime environment
|
||||
ENV ENV_DOCKER=true
|
||||
|
||||
COPY --from=builder /go/src/app/finger /usr/local/bin/finger
|
||||
|
||||
HEALTHCHECK CMD [ "finger", "healthcheck" ]
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT [ "finger" ]
|
||||
CMD [ "serve" ]
|
||||
|
|
2
Makefile
2
Makefile
|
@ -9,7 +9,7 @@ build:
|
|||
test:
|
||||
go test -v ./...
|
||||
|
||||
serve:
|
||||
run:
|
||||
go run main.go serve
|
||||
|
||||
clean:
|
||||
|
|
382
README.md
382
README.md
|
@ -1,148 +1,234 @@
|
|||
# Finger
|
||||
|
||||
Webfinger server written in Go.
|
||||
|
||||
## Features
|
||||
- 🍰 Easy YAML configuration
|
||||
- 🪶 Single 8MB binary / 0% idle CPU / 4MB idle RAM
|
||||
- ⚡️ Sub millisecond responses at 10,000 request per second
|
||||
- 🐳 10MB Docker image
|
||||
|
||||
## Install
|
||||
|
||||
Via `go install`:
|
||||
|
||||
```bash
|
||||
go install git.maronato.dev/maronato/finger@latest
|
||||
```
|
||||
|
||||
Via Docker:
|
||||
|
||||
```bash
|
||||
docker run --name finger /
|
||||
-p 8080:8080 /
|
||||
git.maronato.dev/maronato/finger
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
If you installed it using `go install`, run
|
||||
```bash
|
||||
finger serve
|
||||
```
|
||||
To start the server on port `8080`. Your resources will be queryable via `locahost:8080/.well-known/webfinger?resource=<your-resource>`
|
||||
|
||||
If you're using Docker, the use the same command in the install section.
|
||||
|
||||
By default, no resources will be exposed. You can create resources via a `fingers.yml` file. It should contain a collection of resources as keys and their attributes as their objects.
|
||||
|
||||
Some default URN aliases are provided via the built-in mapping ([`urns.yml`](./urns.yml)). You can replace that with your own or use URNs directly in the `fingers.yml` file.
|
||||
|
||||
Here's an example:
|
||||
```yaml
|
||||
# fingers.yml
|
||||
|
||||
# Resources go in the root of the file. Email address will have the acct:
|
||||
# prefix added automatically.
|
||||
alice@example.com:
|
||||
# "avatar" is an alias of "http://webfinger.net/rel/avatar"
|
||||
# (see urns.yml for more)
|
||||
avatar: "https://example.com/alice-pic"
|
||||
|
||||
# If the value is a URI, it'll be exposed as a webfinger link
|
||||
openid: "https://sso.example.com/"
|
||||
|
||||
# If the value of the attribute is not a URI, it will be exposed as a
|
||||
# webfinger property
|
||||
name: "Alice Doe"
|
||||
|
||||
# You can also specify URN's directly instead of the aliases
|
||||
http://webfinger.net/rel/profile-page: "https://example.com/user/alice"
|
||||
|
||||
bob@example.com:
|
||||
name: Bob Foo
|
||||
openid: "https://sso.example.com/"
|
||||
|
||||
# Resources can also be URIs
|
||||
https://example.com/user/charlie:
|
||||
name: Charlie Baz
|
||||
profile: https://example.com/user/charlie
|
||||
```
|
||||
|
||||
### Example queries
|
||||
<details>
|
||||
<summary><b>Query Alice</b><pre>GET http://localhost:8080/.well-known/webfinger?resource=acct:alice@example.com</pre></summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "acct:alice@example.com",
|
||||
"links": [
|
||||
{
|
||||
"rel": "avatar",
|
||||
"href": "https://example.com/alice-pic"
|
||||
},
|
||||
{
|
||||
"rel": "openid",
|
||||
"href": "https://sso.example.com/"
|
||||
},
|
||||
{
|
||||
"rel": "name",
|
||||
"href": "Alice Doe"
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"href": "https://example.com/user/alice"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>Query Bob</b><pre>GET http://localhost:8080/.well-known/webfinger?resource=acct:bob@example.com</pre></summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "acct:bob@example.com",
|
||||
"links": [
|
||||
{
|
||||
"rel": "name",
|
||||
"href": "Bob Foo"
|
||||
},
|
||||
{
|
||||
"rel": "openid",
|
||||
"href": "https://sso.example.com/"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>Query Charlie</b><pre>GET http://localhost:8080/.well-known/webfinger?resource=https://example.com/user/charlie</pre></summary>
|
||||
|
||||
```JSON
|
||||
{
|
||||
"subject": "https://example.com/user/charlie",
|
||||
"links": [
|
||||
{
|
||||
"rel": "name",
|
||||
"href": "Charlie Baz"
|
||||
},
|
||||
{
|
||||
"rel": "profile",
|
||||
"href": "https://example.com/user/charlie"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Configs
|
||||
Here are the config options available. You can change them via command line flags or environment variables:
|
||||
|
||||
| CLI flag | Env variable | Default | Description |
|
||||
| -------- | ------------ | ------- | ----------- |
|
||||
| fdsfds | gsfgfs | fgfsdgf | gdfsgdf |
|
||||
# Finger
|
||||
|
||||
Webfinger handler / standalone server written in Go.
|
||||
|
||||
## Features
|
||||
- 🍰 Easy YAML configuration
|
||||
- 🪶 Single 8MB binary / 0% idle CPU / 4MB idle RAM
|
||||
- ⚡️ Sub millisecond responses at 10,000 request per second
|
||||
- 🐳 10MB Docker image
|
||||
|
||||
## In your existing server
|
||||
|
||||
To use Finger in your existing server, download the package as a dependency:
|
||||
|
||||
```bash
|
||||
go get git.maronato.dev/maronato/finger@latest
|
||||
```
|
||||
|
||||
Then, use it as a regular `http.Handler`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"git.maronato.dev/maronato/finger/handler"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create the webfingers map that will be served by the handler
|
||||
fingers, err := webfingers.NewWebFingers(
|
||||
// Pass a map of your resources (Subject key followed by it's properties and links)
|
||||
// the syntax is the same as the fingers.yml file (see below)
|
||||
webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"name": "Example User",
|
||||
},
|
||||
},
|
||||
// Optionally, pass a map of URN aliases (see urns.yml for more)
|
||||
// If nil is provided, no aliases will be used
|
||||
webfingers.URNAliases{
|
||||
"name": "http://schema.org/name",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
// Then use the handler as a regular http.Handler
|
||||
mux.Handle("/.well-known/webfinger", handler.WebfingerHandler(fingers))
|
||||
|
||||
log.Fatal(http.ListenAndServe("localhost:8080", mux))
|
||||
}
|
||||
```
|
||||
|
||||
## As a standalone server
|
||||
|
||||
If you don't have a server, Finger can also serve itself. You can install it via `go install` or use the Docker image.
|
||||
|
||||
Via `go install`:
|
||||
|
||||
```bash
|
||||
go install git.maronato.dev/maronato/finger@latest
|
||||
```
|
||||
|
||||
Via Docker:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
--name finger \
|
||||
-p 8080:8080 \
|
||||
-v ${PWD}/fingers.yml:/app/fingers.yml \
|
||||
git.maronato.dev/maronato/finger
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
If you installed it using `go install`, run
|
||||
```bash
|
||||
finger serve
|
||||
```
|
||||
To start the server on port `8080`. Your resources will be queryable via `locahost:8080/.well-known/webfinger?resource=<your-resource>`
|
||||
|
||||
If you're using Docker, the use the same command in the install section.
|
||||
|
||||
By default, no resources will be exposed. You can create resources via a `fingers.yml` file. It should contain a collection of resources as keys and their attributes as their objects.
|
||||
|
||||
Some default URN aliases are provided via the built-in mapping ([`urns.yml`](./urns.yml)). You can replace that with your own or use URNs directly in the `fingers.yml` file.
|
||||
|
||||
Here's an example:
|
||||
```yaml
|
||||
# fingers.yml
|
||||
|
||||
# Resources go in the root of the file. Email address will have the acct:
|
||||
# prefix added automatically.
|
||||
alice@example.com:
|
||||
# "avatar" is an alias of "http://webfinger.net/rel/avatar"
|
||||
# (see urns.yml for more)
|
||||
avatar: "https://example.com/alice-pic"
|
||||
|
||||
# If the value is a URI, it'll be exposed as a webfinger link
|
||||
openid: "https://sso.example.com/"
|
||||
|
||||
# If the value of the attribute is not a URI, it will be exposed as a
|
||||
# webfinger property
|
||||
name: "Alice Doe"
|
||||
|
||||
# You can also specify URN's directly instead of the aliases
|
||||
http://webfinger.net/rel/profile-page: "https://example.com/user/alice"
|
||||
|
||||
bob@example.com:
|
||||
name: Bob Foo
|
||||
openid: "https://sso.example.com/"
|
||||
|
||||
# Resources can also be URIs
|
||||
https://example.com/user/charlie:
|
||||
name: Charlie Baz
|
||||
profile: https://example.com/user/charlie
|
||||
```
|
||||
|
||||
### Example queries
|
||||
<details>
|
||||
<summary><b>Query Alice</b><pre>GET http://localhost:8080/.well-known/webfinger?resource=acct:alice@example.com</pre></summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "acct:alice@example.com",
|
||||
"links": [
|
||||
{
|
||||
"rel": "avatar",
|
||||
"href": "https://example.com/alice-pic"
|
||||
},
|
||||
{
|
||||
"rel": "openid",
|
||||
"href": "https://sso.example.com/"
|
||||
},
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"href": "https://example.com/user/alice"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"name": "Alice Doe"
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>Query Bob</b><pre>GET http://localhost:8080/.well-known/webfinger?resource=acct:bob@example.com</pre></summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "acct:bob@example.com",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://openid.net/specs/connect/1.0/issuer",
|
||||
"href": "https://sso.example.com/"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"http://schema.org/name": "Bob Foo"
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary><b>Query Charlie</b><pre>GET http://localhost:8080/.well-known/webfinger?resource=https://example.com/user/charlie</pre></summary>
|
||||
|
||||
```JSON
|
||||
{
|
||||
"subject": "https://example.com/user/charlie",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"href": "https://example.com/user/charlie"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"http://schema.org/name": "Charlie Baz"
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Commands
|
||||
|
||||
Finger exposes two commands: `serve` and `healthcheck`. `serve` is the default command and starts the server. `healthcheck` is used by the Docker healthcheck to check if the server is up.
|
||||
|
||||
## Configs
|
||||
Here are the config options available. You can change them via command line flags or environment variables:
|
||||
|
||||
| CLI flag | Env variable | Default | Description |
|
||||
| ------------------- | ---------------- | -------------------------------------- | -------------------------------------- |
|
||||
| `-p, --port` | `WF_PORT` | `8080` | Port where the server listens to |
|
||||
| `-h, --host` | `WF_HOST` | `localhost` (`0.0.0.0` when in Docker) | Host where the server listens to |
|
||||
| `-f, --finger-file` | `WF_FINGER_FILE` | `fingers.yml` | Path to the webfingers definition file |
|
||||
| `-u, --urn-file` | `WF_URN_FILE` | `urns.yml` | Path to the URNs alias file |
|
||||
| `-d, --debug` | `WF_DEBUG` | `false` | Enable debug logging |
|
||||
|
||||
### Docker config
|
||||
If you're using the Docker image, you can mount your `fingers.yml` file to `/app/fingers.yml` and the `urns.yml` to `/app/urns.yml`.
|
||||
|
||||
To run the docker image with flags or a different command, specify the command followed by the flags:
|
||||
```bash
|
||||
# Start the server on port 3030 in debug mode with a different fingers file
|
||||
docker run git.maronato.dev/maronato/finger serve --port 3030 --debug --finger-file /app/my-fingers.yml
|
||||
|
||||
# or run a healthcheck on a different finger container
|
||||
docker run git.maronato.dev/maronato/finger healthcheck --host otherhost --port 3030
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
You need to have [Go](https://golang.org/) installed to build the project.
|
||||
|
||||
Clone the repo and run `make build` to build the binary. You can then run `./finger serve` to start the server.
|
||||
|
||||
A few other commands are:
|
||||
- `make run` to run the server
|
||||
- `make test` to run the tests
|
||||
- `make lint` to run the linter
|
||||
- `make clean` to clean the build files
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
|
10
cmd/serve.go
10
cmd/serve.go
|
@ -6,9 +6,9 @@ import (
|
|||
"os"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/fingerreader"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/server"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
"github.com/peterbourgon/ff/v4"
|
||||
)
|
||||
|
||||
|
@ -25,21 +25,21 @@ func newServerCmd(cfg *config.Config) *ff.Command {
|
|||
ctx = log.WithLogger(ctx, l)
|
||||
|
||||
// Read the webfinger files
|
||||
r := webfinger.NewFingerReader()
|
||||
r := fingerreader.NewFingerReader()
|
||||
err := r.ReadFiles(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading finger files: %w", err)
|
||||
}
|
||||
|
||||
webfingers, err := r.ReadFingerFile(ctx)
|
||||
fingers, err := r.ReadFingerFile(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing finger files: %w", err)
|
||||
}
|
||||
|
||||
l.Info(fmt.Sprintf("Loaded %d webfingers", len(webfingers)))
|
||||
l.Info(fmt.Sprintf("Loaded %d webfingers", len(fingers)))
|
||||
|
||||
// Start the server
|
||||
if err := server.StartServer(ctx, cfg, webfingers); err != nil {
|
||||
if err := server.StartServer(ctx, cfg, fingers); err != nil {
|
||||
return fmt.Errorf("error running server: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,16 @@
|
|||
package server
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
)
|
||||
|
||||
func WebfingerHandler(_ *config.Config, webfingers webfinger.WebFingers) http.Handler {
|
||||
func WebfingerHandler(fingers webfingers.WebFingers) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// Only handle GET requests
|
||||
if r.Method != http.MethodGet {
|
||||
l.Debug("Method not allowed")
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
|
||||
return
|
||||
|
@ -28,16 +22,14 @@ func WebfingerHandler(_ *config.Config, webfingers webfinger.WebFingers) http.Ha
|
|||
// Get the resource
|
||||
resource := q.Get("resource")
|
||||
if resource == "" {
|
||||
l.Debug("No resource provided")
|
||||
http.Error(w, "No resource provided", http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get and validate resource
|
||||
finger, ok := webfingers[resource]
|
||||
finger, ok := fingers[resource]
|
||||
if !ok {
|
||||
l.Debug("Resource not found")
|
||||
http.Error(w, "Resource not found", http.StatusNotFound)
|
||||
|
||||
return
|
||||
|
@ -48,12 +40,9 @@ func WebfingerHandler(_ *config.Config, webfingers webfinger.WebFingers) http.Ha
|
|||
|
||||
// Write the response
|
||||
if err := json.NewEncoder(w).Encode(finger); err != nil {
|
||||
l.Debug("Error encoding json")
|
||||
http.Error(w, "Error encoding json", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
l.Debug("Webfinger request successful")
|
||||
})
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package server_test
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -10,19 +10,19 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.maronato.dev/maronato/finger/handler"
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/server"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
)
|
||||
|
||||
func TestWebfingerHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
webfingers := webfinger.WebFingers{
|
||||
fingers := webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Links: []webfinger.Link{
|
||||
Links: []webfingers.Link{
|
||||
{
|
||||
Rel: "http://webfinger.net/rel/profile-page",
|
||||
Href: "https://example.com/user",
|
||||
|
@ -104,7 +104,7 @@ func TestWebfingerHandler(t *testing.T) {
|
|||
w := httptest.NewRecorder()
|
||||
|
||||
// Create a new handler
|
||||
h := server.WebfingerHandler(cfg, webfingers)
|
||||
h := handler.WebfingerHandler(fingers)
|
||||
|
||||
// Serve the request
|
||||
h.ServeHTTP(w, r)
|
||||
|
@ -121,8 +121,8 @@ func TestWebfingerHandler(t *testing.T) {
|
|||
t.Errorf("expected content type %s, got %s", "application/jrd+json", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
fingerWant := webfingers[tc.resource]
|
||||
fingerGot := &webfinger.WebFinger{}
|
||||
fingerWant := fingers[tc.resource]
|
||||
fingerGot := &webfingers.WebFinger{}
|
||||
|
||||
// Decode the response body
|
||||
if err := json.NewDecoder(w.Body).Decode(fingerGot); err != nil {
|
||||
|
@ -147,3 +147,30 @@ func TestWebfingerHandler(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWebfingerHandler(b *testing.B) {
|
||||
fingers, err := webfingers.NewWebFingers(
|
||||
webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
h := handler.WebfingerHandler(fingers)
|
||||
r := httptest.NewRequest(http.MethodGet, "/.well-known/webfinger?resource=acct:user@example.com", http.NoBody)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
b.Errorf("expected status code %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
89
internal/fingerreader/fingerreader.go
Normal file
89
internal/fingerreader/fingerreader.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package fingerreader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type FingerReader struct {
|
||||
URNSFile []byte
|
||||
FingersFile []byte
|
||||
}
|
||||
|
||||
func NewFingerReader() *FingerReader {
|
||||
return &FingerReader{}
|
||||
}
|
||||
|
||||
func (f *FingerReader) ReadFiles(cfg *config.Config) error {
|
||||
// Read URNs file
|
||||
file, err := os.ReadFile(cfg.URNPath)
|
||||
if err != nil {
|
||||
// If the file does not exist and the path is the default, set the URNs to an empty map
|
||||
if os.IsNotExist(err) && cfg.URNPath == config.DefaultURNPath {
|
||||
f.URNSFile = []byte("")
|
||||
} else {
|
||||
return fmt.Errorf("error opening URNs file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f.URNSFile = file
|
||||
|
||||
// Read fingers file
|
||||
file, err = os.ReadFile(cfg.FingerPath)
|
||||
if err != nil {
|
||||
// If the file does not exist and the path is the default, set the fingers to an empty map
|
||||
if os.IsNotExist(err) && cfg.FingerPath == config.DefaultFingerPath {
|
||||
f.FingersFile = []byte("")
|
||||
} else {
|
||||
return fmt.Errorf("error opening fingers file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f.FingersFile = file
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FingerReader) ReadFingerFile(ctx context.Context) (webfingers.WebFingers, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
urnAliases := make(webfingers.URNAliases)
|
||||
resources := make(webfingers.Resources)
|
||||
|
||||
// Parse the URNs file
|
||||
if err := yaml.Unmarshal(f.URNSFile, &urnAliases); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling URNs file: %w", err)
|
||||
}
|
||||
|
||||
// The URNs file must be a map of strings to valid URLs
|
||||
for _, v := range urnAliases {
|
||||
if _, err := url.ParseRequestURI(v); err != nil {
|
||||
return nil, fmt.Errorf("error parsing URN URIs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
l.Debug("URNs file parsed successfully", slog.Int("number", len(urnAliases)), slog.Any("data", urnAliases))
|
||||
|
||||
// Parse the fingers file
|
||||
if err := yaml.Unmarshal(f.FingersFile, &resources); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling fingers file: %w", err)
|
||||
}
|
||||
|
||||
l.Debug("Fingers file parsed successfully", slog.Int("number", len(resources)), slog.Any("data", resources))
|
||||
|
||||
// Parse raw data
|
||||
fingers, err := webfingers.NewWebFingers(resources, urnAliases)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing raw fingers: %w", err)
|
||||
}
|
||||
|
||||
return fingers, nil
|
||||
}
|
242
internal/fingerreader/fingerreader_test.go
Normal file
242
internal/fingerreader/fingerreader_test.go
Normal file
|
@ -0,0 +1,242 @@
|
|||
package fingerreader_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/fingerreader"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
)
|
||||
|
||||
func newTempFile(t *testing.T, content string) (name string, remove func()) {
|
||||
t.Helper()
|
||||
|
||||
f, err := os.CreateTemp("", "finger-test")
|
||||
if err != nil {
|
||||
t.Fatalf("error creating temp file: %v", err)
|
||||
}
|
||||
|
||||
_, err = f.WriteString(content)
|
||||
if err != nil {
|
||||
t.Fatalf("error writing to temp file: %v", err)
|
||||
}
|
||||
|
||||
return f.Name(), func() {
|
||||
err = os.Remove(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("error removing temp file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFingerReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := fingerreader.NewFingerReader()
|
||||
|
||||
if f == nil {
|
||||
t.Errorf("NewFingerReader() = %v, want: %v", f, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerReader_ReadFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
urnsContent string
|
||||
fingersContent string
|
||||
useURNFile bool
|
||||
useFingerFile bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "reads files",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
useURNFile: true,
|
||||
useFingerFile: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on missing URNs file",
|
||||
urnsContent: "invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
useURNFile: false,
|
||||
useFingerFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on missing fingers file",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid",
|
||||
useFingerFile: false,
|
||||
useURNFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.NewConfig()
|
||||
|
||||
urnsFileName, urnsCleanup := newTempFile(t, tc.urnsContent)
|
||||
defer urnsCleanup()
|
||||
|
||||
fingersFileName, fingersCleanup := newTempFile(t, tc.fingersContent)
|
||||
defer fingersCleanup()
|
||||
|
||||
if !tc.useURNFile {
|
||||
cfg.URNPath = "invalid"
|
||||
} else {
|
||||
cfg.URNPath = urnsFileName
|
||||
}
|
||||
|
||||
if !tc.useFingerFile {
|
||||
cfg.FingerPath = "invalid"
|
||||
} else {
|
||||
cfg.FingerPath = fingersFileName
|
||||
}
|
||||
|
||||
f := fingerreader.NewFingerReader()
|
||||
|
||||
err := f.ReadFiles(cfg)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("ReadFiles() error = %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Errorf("ReadFiles() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(f.URNSFile, []byte(tc.urnsContent)) {
|
||||
t.Errorf("ReadFiles() URNsFile = %v, want: %v", f.URNSFile, tc.urnsContent)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(f.FingersFile, []byte(tc.fingersContent)) {
|
||||
t.Errorf("ReadFiles() FingersFile = %v, want: %v", f.FingersFile, tc.fingersContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFingerFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
urnsContent string
|
||||
fingersContent string
|
||||
wantURN webfingers.URNAliases
|
||||
wantFinger webfingers.Resources
|
||||
returns webfingers.WebFingers
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "reads files",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantURN: webfingers.URNAliases{
|
||||
"name": "https://schema/name",
|
||||
"profile": "https://schema/profile",
|
||||
},
|
||||
wantFinger: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
returns: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "uses custom URNs",
|
||||
urnsContent: "favorite_food: https://schema/favorite_food",
|
||||
fingersContent: "user@example.com:\n favorite_food: Apple",
|
||||
wantURN: webfingers.URNAliases{
|
||||
"favorite_food": "https://schema/favorite_food",
|
||||
},
|
||||
wantFinger: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"https://schema/favorite_food": "Apple",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid URNs file",
|
||||
urnsContent: "invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid fingers file",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid URNs values",
|
||||
urnsContent: "name: invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid fingers values",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid:\n name: John Doe",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
cfg := config.NewConfig()
|
||||
l := log.NewLogger(&strings.Builder{}, cfg)
|
||||
|
||||
ctx = log.WithLogger(ctx, l)
|
||||
|
||||
f := fingerreader.NewFingerReader()
|
||||
|
||||
f.FingersFile = []byte(tc.fingersContent)
|
||||
f.URNSFile = []byte(tc.urnsContent)
|
||||
|
||||
got, err := f.ReadFingerFile(ctx)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("ReadFingerFile() error = %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Errorf("ReadFingerFile() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
|
||||
if tc.returns != nil && !reflect.DeepEqual(got, tc.returns) {
|
||||
t.Errorf("ReadFingerFile() got = %v, want: %v", got, tc.returns)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -8,10 +8,11 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.maronato.dev/maronato/finger/handler"
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/middleware"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
|
@ -33,20 +34,17 @@ const (
|
|||
RequestTimeout = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
func StartServer(ctx context.Context, cfg *config.Config, webfingers webfinger.WebFingers) error {
|
||||
func StartServer(ctx context.Context, cfg *config.Config, fingers webfingers.WebFingers) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// Create the server mux
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/.well-known/webfinger", WebfingerHandler(cfg, webfingers))
|
||||
mux.Handle("/.well-known/webfinger", handler.WebfingerHandler(fingers))
|
||||
mux.Handle("/healthz", HealthCheckHandler(cfg))
|
||||
|
||||
// Create a new server
|
||||
srv := &http.Server{
|
||||
Addr: cfg.GetAddr(),
|
||||
BaseContext: func(_ net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
Handler: middleware.RequestLogger(
|
||||
middleware.Recoverer(
|
||||
http.TimeoutHandler(mux, RequestTimeout, "request timed out"),
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/server"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
)
|
||||
|
||||
func getPortGenerator() func() int {
|
||||
|
@ -96,8 +96,8 @@ func TestStartServer(t *testing.T) {
|
|||
cfg.Port = fmt.Sprint(portGenerator())
|
||||
|
||||
resource := "acct:user@example.com"
|
||||
webfingers := webfinger.WebFingers{
|
||||
resource: &webfinger.WebFinger{
|
||||
fingers := webfingers.WebFingers{
|
||||
resource: &webfingers.WebFinger{
|
||||
Subject: resource,
|
||||
Properties: map[string]string{
|
||||
"http://webfinger.net/rel/name": "John Doe",
|
||||
|
@ -107,7 +107,7 @@ func TestStartServer(t *testing.T) {
|
|||
|
||||
go func() {
|
||||
// Start the server
|
||||
err := server.StartServer(ctx, cfg, webfingers)
|
||||
err := server.StartServer(ctx, cfg, fingers)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ func TestStartServer(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the response body
|
||||
fingerGot := &webfinger.WebFinger{}
|
||||
fingerGot := &webfingers.WebFinger{}
|
||||
|
||||
// Decode the response body
|
||||
if err := json.NewDecoder(resp.Body).Decode(fingerGot); err != nil {
|
||||
|
@ -148,7 +148,7 @@ func TestStartServer(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the response body
|
||||
fingerWant := webfingers[resource]
|
||||
fingerWant := fingers[resource]
|
||||
|
||||
if !reflect.DeepEqual(fingerGot, fingerWant) {
|
||||
t.Errorf("expected %v, got %v", fingerWant, fingerGot)
|
||||
|
|
|
@ -1,170 +0,0 @@
|
|||
package webfinger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
Rel string `json:"rel"`
|
||||
Href string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
type WebFinger struct {
|
||||
Subject string `json:"subject"`
|
||||
Links []Link `json:"links,omitempty"`
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type WebFingers map[string]*WebFinger
|
||||
|
||||
type (
|
||||
URNMap = map[string]string
|
||||
RawFingersMap = map[string]map[string]string
|
||||
)
|
||||
|
||||
type FingerReader struct {
|
||||
URNSFile []byte
|
||||
FingersFile []byte
|
||||
}
|
||||
|
||||
func NewFingerReader() *FingerReader {
|
||||
return &FingerReader{}
|
||||
}
|
||||
|
||||
func (f *FingerReader) ReadFiles(cfg *config.Config) error {
|
||||
// Read URNs file
|
||||
file, err := os.ReadFile(cfg.URNPath)
|
||||
if err != nil {
|
||||
// If the file does not exist and the path is the default, set the URNs to an empty map
|
||||
if os.IsNotExist(err) && cfg.URNPath == config.DefaultURNPath {
|
||||
f.URNSFile = []byte("")
|
||||
} else {
|
||||
return fmt.Errorf("error opening URNs file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f.URNSFile = file
|
||||
|
||||
// Read fingers file
|
||||
file, err = os.ReadFile(cfg.FingerPath)
|
||||
if err != nil {
|
||||
// If the file does not exist and the path is the default, set the fingers to an empty map
|
||||
if os.IsNotExist(err) && cfg.FingerPath == config.DefaultFingerPath {
|
||||
f.FingersFile = []byte("")
|
||||
} else {
|
||||
return fmt.Errorf("error opening fingers file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f.FingersFile = file
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FingerReader) ParseFingers(ctx context.Context, urns URNMap, rawFingers RawFingersMap) (WebFingers, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
webfingers := make(WebFingers)
|
||||
|
||||
// Parse the webfinger file
|
||||
for k, v := range rawFingers {
|
||||
resource := k
|
||||
|
||||
// Remove leading acct: if present
|
||||
if len(k) > 5 && resource[:5] == "acct:" {
|
||||
resource = resource[5:]
|
||||
}
|
||||
|
||||
// The key must be a URL or email address
|
||||
if _, err := mail.ParseAddress(resource); err != nil {
|
||||
if _, err := url.ParseRequestURI(resource); err != nil {
|
||||
return nil, fmt.Errorf("error parsing webfinger key (%s): %w", k, err)
|
||||
}
|
||||
} else {
|
||||
// Add acct: back to the key if it is an email address
|
||||
resource = fmt.Sprintf("acct:%s", resource)
|
||||
}
|
||||
|
||||
// Create a new webfinger
|
||||
webfinger := &WebFinger{
|
||||
Subject: resource,
|
||||
}
|
||||
|
||||
// Parse the fields
|
||||
for field, value := range v {
|
||||
fieldUrn := field
|
||||
|
||||
// If the key is present in the URNs file, use the value
|
||||
if _, ok := urns[field]; ok {
|
||||
fieldUrn = urns[field]
|
||||
}
|
||||
|
||||
// If the value is a valid URI, add it to the links
|
||||
if _, err := url.ParseRequestURI(value); err == nil {
|
||||
webfinger.Links = append(webfinger.Links, Link{
|
||||
Rel: fieldUrn,
|
||||
Href: value,
|
||||
})
|
||||
} else {
|
||||
// Otherwise add it to the properties
|
||||
if webfinger.Properties == nil {
|
||||
webfinger.Properties = make(map[string]string)
|
||||
}
|
||||
|
||||
webfinger.Properties[fieldUrn] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Add the webfinger to the map
|
||||
webfingers[resource] = webfinger
|
||||
}
|
||||
|
||||
l.Debug("Webfinger map built successfully", slog.Int("number", len(webfingers)), slog.Any("data", webfingers))
|
||||
|
||||
return webfingers, nil
|
||||
}
|
||||
|
||||
func (f *FingerReader) ReadFingerFile(ctx context.Context) (WebFingers, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
urnMap := make(URNMap)
|
||||
fingerData := make(RawFingersMap)
|
||||
|
||||
// Parse the URNs file
|
||||
if err := yaml.Unmarshal(f.URNSFile, &urnMap); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling URNs file: %w", err)
|
||||
}
|
||||
|
||||
// The URNs file must be a map of strings to valid URLs
|
||||
for _, v := range urnMap {
|
||||
if _, err := url.ParseRequestURI(v); err != nil {
|
||||
return nil, fmt.Errorf("error parsing URN URIs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
l.Debug("URNs file parsed successfully", slog.Int("number", len(urnMap)), slog.Any("data", urnMap))
|
||||
|
||||
// Parse the fingers file
|
||||
if err := yaml.Unmarshal(f.FingersFile, &fingerData); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling fingers file: %w", err)
|
||||
}
|
||||
|
||||
l.Debug("Fingers file parsed successfully", slog.Int("number", len(fingerData)), slog.Any("data", fingerData))
|
||||
|
||||
// Parse raw data
|
||||
webfingers, err := f.ParseFingers(ctx, urnMap, fingerData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing raw fingers: %w", err)
|
||||
}
|
||||
|
||||
return webfingers, nil
|
||||
}
|
|
@ -1,444 +0,0 @@
|
|||
package webfinger_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.maronato.dev/maronato/finger/internal/config"
|
||||
"git.maronato.dev/maronato/finger/internal/log"
|
||||
"git.maronato.dev/maronato/finger/internal/webfinger"
|
||||
)
|
||||
|
||||
func newTempFile(t *testing.T, content string) (name string, remove func()) {
|
||||
t.Helper()
|
||||
|
||||
f, err := os.CreateTemp("", "finger-test")
|
||||
if err != nil {
|
||||
t.Fatalf("error creating temp file: %v", err)
|
||||
}
|
||||
|
||||
_, err = f.WriteString(content)
|
||||
if err != nil {
|
||||
t.Fatalf("error writing to temp file: %v", err)
|
||||
}
|
||||
|
||||
return f.Name(), func() {
|
||||
err = os.Remove(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("error removing temp file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFingerReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := webfinger.NewFingerReader()
|
||||
|
||||
if f == nil {
|
||||
t.Errorf("NewFingerReader() = %v, want: %v", f, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFingerReader_ReadFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
urnsContent string
|
||||
fingersContent string
|
||||
useURNFile bool
|
||||
useFingerFile bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "reads files",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
useURNFile: true,
|
||||
useFingerFile: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on missing URNs file",
|
||||
urnsContent: "invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
useURNFile: false,
|
||||
useFingerFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on missing fingers file",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid",
|
||||
useFingerFile: false,
|
||||
useURNFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.NewConfig()
|
||||
|
||||
urnsFileName, urnsCleanup := newTempFile(t, tc.urnsContent)
|
||||
defer urnsCleanup()
|
||||
|
||||
fingersFileName, fingersCleanup := newTempFile(t, tc.fingersContent)
|
||||
defer fingersCleanup()
|
||||
|
||||
if !tc.useURNFile {
|
||||
cfg.URNPath = "invalid"
|
||||
} else {
|
||||
cfg.URNPath = urnsFileName
|
||||
}
|
||||
|
||||
if !tc.useFingerFile {
|
||||
cfg.FingerPath = "invalid"
|
||||
} else {
|
||||
cfg.FingerPath = fingersFileName
|
||||
}
|
||||
|
||||
f := webfinger.NewFingerReader()
|
||||
|
||||
err := f.ReadFiles(cfg)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("ReadFiles() error = %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Errorf("ReadFiles() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(f.URNSFile, []byte(tc.urnsContent)) {
|
||||
t.Errorf("ReadFiles() URNsFile = %v, want: %v", f.URNSFile, tc.urnsContent)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(f.FingersFile, []byte(tc.fingersContent)) {
|
||||
t.Errorf("ReadFiles() FingersFile = %v, want: %v", f.FingersFile, tc.fingersContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFingers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rawFingers webfinger.RawFingersMap
|
||||
want webfinger.WebFingers
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "parses links",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"profile": "https://example.com/profile",
|
||||
"invalidalias": "https://example.com/invalidalias",
|
||||
"https://something": "https://somethingelse",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Links: []webfinger.Link{
|
||||
{
|
||||
Rel: "https://schema/profile",
|
||||
Href: "https://example.com/profile",
|
||||
},
|
||||
{
|
||||
Rel: "invalidalias",
|
||||
Href: "https://example.com/invalidalias",
|
||||
},
|
||||
{
|
||||
Rel: "https://something",
|
||||
Href: "https://somethingelse",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "parses properties",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"name": "John Doe",
|
||||
"invalidalias": "value1",
|
||||
"https://mylink": "value2",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
"invalidalias": "value1",
|
||||
"https://mylink": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "accepts acct: prefix",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"acct:user@example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "accepts urls as resource",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"https://example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"https://example.com": {
|
||||
Subject: "https://example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "accepts multiple resources",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
"other@example.com": {
|
||||
"name": "Jane Doe",
|
||||
},
|
||||
},
|
||||
want: webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
"acct:other@example.com": {
|
||||
Subject: "acct:other@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "Jane Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid resource",
|
||||
rawFingers: webfinger.RawFingersMap{
|
||||
"invalid": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a urn map
|
||||
urns := webfinger.URNMap{
|
||||
"name": "https://schema/name",
|
||||
"profile": "https://schema/profile",
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cfg := config.NewConfig()
|
||||
l := log.NewLogger(&strings.Builder{}, cfg)
|
||||
|
||||
ctx = log.WithLogger(ctx, l)
|
||||
|
||||
f := webfinger.NewFingerReader()
|
||||
|
||||
got, err := f.ParseFingers(ctx, urns, tc.rawFingers)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("ParseFingers() error = %v, wantErr %v", err, tc.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Sort links to make it easier to compare
|
||||
for _, v := range got {
|
||||
for range v.Links {
|
||||
sort.Slice(v.Links, func(i, j int) bool {
|
||||
return v.Links[i].Rel < v.Links[j].Rel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range tc.want {
|
||||
for range v.Links {
|
||||
sort.Slice(v.Links, func(i, j int) bool {
|
||||
return v.Links[i].Rel < v.Links[j].Rel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
// Unmarshal the structs to JSON to make it easier to print
|
||||
gotstr := &strings.Builder{}
|
||||
gotenc := json.NewEncoder(gotstr)
|
||||
|
||||
wantstr := &strings.Builder{}
|
||||
wantenc := json.NewEncoder(wantstr)
|
||||
|
||||
_ = gotenc.Encode(got)
|
||||
_ = wantenc.Encode(tc.want)
|
||||
|
||||
t.Errorf("ParseFingers() got = \n%s want: \n%s", gotstr.String(), wantstr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFingerFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
urnsContent string
|
||||
fingersContent string
|
||||
wantURN webfinger.URNMap
|
||||
wantFinger webfinger.RawFingersMap
|
||||
returns *webfinger.WebFingers
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "reads files",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantURN: webfinger.URNMap{
|
||||
"name": "https://schema/name",
|
||||
"profile": "https://schema/profile",
|
||||
},
|
||||
wantFinger: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"name": "John Doe",
|
||||
},
|
||||
},
|
||||
returns: &webfinger.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"https://schema/name": "John Doe",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "uses custom URNs",
|
||||
urnsContent: "favorite_food: https://schema/favorite_food",
|
||||
fingersContent: "user@example.com:\n favorite_food: Apple",
|
||||
wantURN: webfinger.URNMap{
|
||||
"favorite_food": "https://schema/favorite_food",
|
||||
},
|
||||
wantFinger: webfinger.RawFingersMap{
|
||||
"user@example.com": {
|
||||
"https://schema/favorite_food": "Apple",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid URNs file",
|
||||
urnsContent: "invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantURN: webfinger.URNMap{},
|
||||
wantFinger: webfinger.RawFingersMap{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid fingers file",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid",
|
||||
wantURN: webfinger.URNMap{},
|
||||
wantFinger: webfinger.RawFingersMap{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid URNs values",
|
||||
urnsContent: "name: invalid",
|
||||
fingersContent: "user@example.com:\n name: John Doe",
|
||||
wantURN: webfinger.URNMap{},
|
||||
wantFinger: webfinger.RawFingersMap{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors on invalid fingers values",
|
||||
urnsContent: "name: https://schema/name\nprofile: https://schema/profile",
|
||||
fingersContent: "invalid:\n name: John Doe",
|
||||
wantURN: webfinger.URNMap{},
|
||||
wantFinger: webfinger.RawFingersMap{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
cfg := config.NewConfig()
|
||||
l := log.NewLogger(&strings.Builder{}, cfg)
|
||||
|
||||
ctx = log.WithLogger(ctx, l)
|
||||
|
||||
f := webfinger.NewFingerReader()
|
||||
|
||||
f.FingersFile = []byte(tc.fingersContent)
|
||||
f.URNSFile = []byte(tc.urnsContent)
|
||||
|
||||
got, err := f.ReadFingerFile(ctx)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("ReadFingerFile() error = %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Errorf("ReadFingerFile() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
|
||||
if tc.returns != nil && !reflect.DeepEqual(got, *tc.returns) {
|
||||
t.Errorf("ReadFingerFile() got = %v, want: %v", got, *tc.returns)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
94
webfingers/webfingers.go
Normal file
94
webfingers/webfingers.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package webfingers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Link is a link in a webfinger.
|
||||
type Link struct {
|
||||
Rel string `json:"rel"`
|
||||
Href string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
// WebFinger is a webfinger.
|
||||
type WebFinger struct {
|
||||
Subject string `json:"subject"`
|
||||
Links []Link `json:"links,omitempty"`
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// Resources is a simplified webfinger map.
|
||||
type Resources map[string]map[string]string
|
||||
|
||||
// URNAliases is a map of URN aliases.
|
||||
type URNAliases map[string]string
|
||||
|
||||
// WebFingers is a map of webfingers.
|
||||
type WebFingers map[string]*WebFinger
|
||||
|
||||
// NewWebFingers creates a new webfinger map from a simplified webfinger map and an optional URN aliases map.
|
||||
func NewWebFingers(resources Resources, urnAliases URNAliases) (WebFingers, error) {
|
||||
fingers := make(WebFingers)
|
||||
|
||||
// If the aliases map is nil, create an empty one.
|
||||
if urnAliases == nil {
|
||||
urnAliases = make(URNAliases)
|
||||
}
|
||||
|
||||
// Parse the resources.
|
||||
for k, v := range resources {
|
||||
subject := k
|
||||
|
||||
// Remove leading acct: if present.
|
||||
if len(k) > 5 && subject[:5] == "acct:" {
|
||||
subject = subject[5:]
|
||||
}
|
||||
|
||||
// The subject must be a URL or email address.
|
||||
if _, err := mail.ParseAddress(subject); err != nil {
|
||||
if _, err := url.ParseRequestURI(subject); err != nil {
|
||||
return nil, fmt.Errorf("error parsing resource subject (%s): %w", k, err)
|
||||
}
|
||||
} else {
|
||||
// Add acct: back to the subject if it is an email address.
|
||||
subject = fmt.Sprintf("acct:%s", subject)
|
||||
}
|
||||
|
||||
// Create a new webfinger.
|
||||
finger := &WebFinger{
|
||||
Subject: subject,
|
||||
}
|
||||
|
||||
// Parse the resource fields.
|
||||
for field, value := range v {
|
||||
fieldUrn := field
|
||||
|
||||
// If the key is present in the aliases map, use its value.
|
||||
if _, ok := urnAliases[field]; ok {
|
||||
fieldUrn = urnAliases[field]
|
||||
}
|
||||
|
||||
// If the value is a valid URI, add it to the links.
|
||||
if _, err := url.ParseRequestURI(value); err == nil {
|
||||
finger.Links = append(finger.Links, Link{
|
||||
Rel: fieldUrn,
|
||||
Href: value,
|
||||
})
|
||||
} else {
|
||||
// Otherwise add it to the properties.
|
||||
if finger.Properties == nil {
|
||||
finger.Properties = make(map[string]string)
|
||||
}
|
||||
|
||||
finger.Properties[fieldUrn] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Add the webfinger to the map.
|
||||
fingers[subject] = finger
|
||||
}
|
||||
|
||||
return fingers, nil
|
||||
}
|
231
webfingers/webfingers_test.go
Normal file
231
webfingers/webfingers_test.go
Normal file
|
@ -0,0 +1,231 @@
|
|||
package webfingers_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"git.maronato.dev/maronato/finger/webfingers"
|
||||
)
|
||||
|
||||
func TestNewWebFingers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
resources webfingers.Resources
|
||||
urnAliases webfingers.URNAliases
|
||||
want webfingers.WebFingers
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"name": "Example User",
|
||||
},
|
||||
},
|
||||
urnAliases: webfingers.URNAliases{
|
||||
"name": "http://schema.org/name",
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"http://schema.org/name": "Example User",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses links",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"link1": "https://example.com/link1",
|
||||
"link2": "https://example.com/link2",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Links: []webfingers.Link{
|
||||
{
|
||||
Rel: "link1",
|
||||
Href: "https://example.com/link1",
|
||||
},
|
||||
{
|
||||
Rel: "link2",
|
||||
Href: "https://example.com/link2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses links with URN aliases",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"link1": "https://example.com/link1",
|
||||
},
|
||||
},
|
||||
urnAliases: webfingers.URNAliases{
|
||||
"link1": "http://schema.com/link",
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Links: []webfingers.Link{
|
||||
{
|
||||
Rel: "http://schema.com/link",
|
||||
Href: "https://example.com/link1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses properties",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"prop1": "value1",
|
||||
"prop2": "value2",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"prop1": "value1",
|
||||
"prop2": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses properties with URN aliases",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
urnAliases: webfingers.URNAliases{
|
||||
"prop1": "http://schema.com/prop",
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"http://schema.com/prop": "value1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses multiple resources",
|
||||
resources: webfingers.Resources{
|
||||
"user@example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
"user2@example.com": {
|
||||
"prop2": "value2",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
"acct:user2@example.com": {
|
||||
Subject: "acct:user2@example.com",
|
||||
Properties: map[string]string{
|
||||
"prop2": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses URI resources",
|
||||
resources: webfingers.Resources{
|
||||
"https://example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"https://example.com": {
|
||||
Subject: "https://example.com",
|
||||
Properties: map[string]string{
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parses email resource with acct:",
|
||||
resources: webfingers.Resources{
|
||||
"acct:user@example.com": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
want: webfingers.WebFingers{
|
||||
"acct:user@example.com": {
|
||||
Subject: "acct:user@example.com",
|
||||
Properties: map[string]string{
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "errors on invalid resource",
|
||||
resources: webfingers.Resources{
|
||||
"invalid": {
|
||||
"prop1": "value1",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := webfingers.NewWebFingers(tc.resources, tc.urnAliases)
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
} else if tc.wantErr {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
|
||||
// Sort the links.
|
||||
for _, finger := range got {
|
||||
sort.Slice(finger.Links, func(i, j int) bool {
|
||||
return finger.Links[i].Rel < finger.Links[j].Rel
|
||||
})
|
||||
}
|
||||
|
||||
for _, finger := range tc.want {
|
||||
sort.Slice(finger.Links, func(i, j int) bool {
|
||||
return finger.Links[i].Rel < finger.Links[j].Rel
|
||||
})
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
// Marshall both so we can visualize the differences.
|
||||
gotJSON, _ := json.MarshalIndent(got, "", " ")
|
||||
wantJSON, _ := json.MarshalIndent(tc.want, "", " ")
|
||||
|
||||
t.Errorf("got:\n%s\nwant:\n%s", gotJSON, wantJSON)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue