Tests: Add unit tests for recent metadata, auth, and config changes

This commit is contained in:
Ömer Duran 2026-06-26 13:53:57 +02:00
parent 9fe532914d
commit c7fa52cf13
No known key found for this signature in database
GPG key ID: 2550B0D579890013
5 changed files with 120 additions and 4 deletions

View file

@ -165,6 +165,35 @@ describe("common/config", () => {
expect(config.getIcon()).toBe("/i/pro-1/_theme/logo.svg"); expect(config.getIcon()).toBe("/i/pro-1/_theme/logo.svg");
}); });
it("getIcon resolves a built-in appIcon name against the static uri", () => {
const config = new Config(new StorageShim(), { siteTitle: "Foo", baseUri: "/i/pro-1", appIcon: "bloom" });
expect(config.getIcon()).toBe("/i/pro-1/static/icons/bloom.svg");
});
it("getIcon resolves each selectable app-icon variant to its static svg", () => {
["crisp", "mint", "bold", "bloom", "flower", "ring", "shutter"].forEach((name) => {
const config = new Config(new StorageShim(), { siteTitle: "Foo", appIcon: name });
expect(config.getIcon()).toBe(`/static/icons/${name}.svg`);
});
});
it("getIcon falls back to the logo for an unknown or empty app-icon name", () => {
expect(new Config(new StorageShim(), { siteTitle: "Foo", appIcon: "does-not-exist" }).getIcon()).toBe("/static/icons/logo.svg");
expect(new Config(new StorageShim(), { siteTitle: "Foo" }).getIcon()).toBe("/static/icons/logo.svg");
});
it("getIcon prefers a theme-provided icon over the appIcon setting", () => {
const config = new Config(new StorageShim(), {
siteTitle: "Foo",
baseUri: "/i/pro-1",
appIcon: "bloom",
settings: { ui: { theme: "branded-icon" } },
});
themes.Set("branded-icon", { name: "branded-icon", title: "Branded", colors: {}, variables: { icon: "logo.svg" } });
config.setTheme("branded-icon");
expect(config.getIcon()).toBe("/i/pro-1/_theme/logo.svg");
});
it("should store values", () => { it("should store values", () => {
const storage = new StorageShim(); const storage = new StorageShim();
const values = { siteTitle: "Foo", country: "Germany", city: "Hamburg" }; const values = { siteTitle: "Foo", country: "Germany", city: "Hamburg" };

View file

@ -142,6 +142,37 @@ func TestAuthAny_AppPasswordsDisabled(t *testing.T) {
assert.Equal(t, http.StatusForbidden, s2.HttpStatus()) assert.Equal(t, http.StatusForbidden, s2.HttpStatus())
}) })
} }
// Negative control: the gate keys on IsApplication(), so a non-application
// session (here a client-credentials grant) must keep authorizing whether or
// not app passwords are disabled, proving the flag does not over-reject.
t.Run("NonApplicationSessionUnaffected", func(t *testing.T) {
sess, err := entity.AddClientSession("alice-client-cred", conf.SessionMaxAge(), "*", authn.GrantClientCredentials, nil)
require.NoError(t, err)
require.False(t, sess.IsApplication())
token := sess.AuthToken()
authPhotos := func() *entity.Session {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req, _ := http.NewRequest(http.MethodGet, "/api/v1/photos", nil)
header.SetAuthorization(req, token)
req.RemoteAddr = "10.9.8.7:4321"
c.Request = req
return AuthAny(c, acl.ResourcePhotos, acl.Permissions{acl.ActionView})
}
conf.Settings().Features.AppPasswords = true
s := authPhotos()
require.NotNil(t, s)
assert.Equal(t, http.StatusOK, s.HttpStatus())
conf.Settings().Features.AppPasswords = false
s2 := authPhotos()
require.NotNil(t, s2)
assert.Equal(t, http.StatusOK, s2.HttpStatus(), "disabling app passwords must not reject non-application sessions")
})
} }
func TestAuthAny_AppPasswordWebLoginDisabled(t *testing.T) { func TestAuthAny_AppPasswordWebLoginDisabled(t *testing.T) {

View file

@ -2,6 +2,8 @@ package api
import ( import (
"encoding/json" "encoding/json"
"net/http"
"strconv"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -94,6 +96,32 @@ func TestGetFoldersOriginals(t *testing.T) {
}) })
} }
func TestSearchFolders_Headers(t *testing.T) {
app, router, conf := NewApiTest()
_ = conf.CreateDirectories()
SearchFoldersOriginals(router)
// Request uncached results so the count headers are always written
// (a cache hit returns early before the headers are added).
r := PerformRequest(app, "GET", "/api/v1/folders/originals?uncached=true")
assert.Equal(t, http.StatusOK, r.Code)
result := r.Result()
xFiles, err := strconv.Atoi(result.Header.Get("X-Files"))
assert.NoError(t, err, "X-Files header should be an integer")
xFolders, err := strconv.Atoi(result.Header.Get("X-Folders"))
assert.NoError(t, err, "X-Folders header should be an integer")
xCount, err := strconv.Atoi(result.Header.Get("X-Count"))
assert.NoError(t, err, "X-Count header should be an integer")
// X-Count is the combined number of files and folders returned.
assert.Equal(t, xFiles+xFolders, xCount)
// An unpaginated request reports the form's zero-value count and offset.
assert.Equal(t, "0", result.Header.Get("X-Limit"))
assert.Equal(t, "0", result.Header.Get("X-Offset"))
}
func TestGetFoldersImport(t *testing.T) { func TestGetFoldersImport(t *testing.T) {
t.Run("Flat", func(t *testing.T) { t.Run("Flat", func(t *testing.T) {
app, router, conf := NewApiTest() app, router, conf := NewApiTest()

View file

@ -2,8 +2,10 @@ package entity
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
"time" "time"
"unicode/utf8"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -1645,10 +1647,21 @@ func TestPhoto_ArchiveRestore(t *testing.T) {
} }
func TestPhoto_SetCameraSerial(t *testing.T) { func TestPhoto_SetCameraSerial(t *testing.T) {
m := &Photo{} t.Run("Success", func(t *testing.T) {
assert.Empty(t, m.CameraSerial) m := &Photo{}
m.SetCameraSerial("abcCamera") assert.Empty(t, m.CameraSerial)
assert.Equal(t, "abcCamera", m.CameraSerial) m.SetCameraSerial("abcCamera")
assert.Equal(t, "abcCamera", m.CameraSerial)
})
t.Run("MultiByteClippedOnRuneBoundary", func(t *testing.T) {
// camera_serial is VARBINARY(160), so a long multi-byte value must be byte-clipped
// without splitting a rune, keeping the stored value within budget and valid UTF-8.
m := &Photo{}
m.SetCameraSerial(strings.Repeat("世", 100)) // 100 runes x 3 bytes = 300 bytes.
assert.NotEmpty(t, m.CameraSerial)
assert.LessOrEqual(t, len(m.CameraSerial), txt.ClipDefault)
assert.True(t, utf8.ValidString(m.CameraSerial))
})
} }
func TestPhoto_MapKey(t *testing.T) { func TestPhoto_MapKey(t *testing.T) {

View file

@ -27,3 +27,18 @@ func TestNewLens(t *testing.T) {
assert.Equal(t, "New Model", result.LensModel) assert.Equal(t, "New Model", result.LensModel)
}) })
} }
func TestLens_Validate(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
frm := &Lens{LensMake: "Canon", LensModel: "EF 50mm f/1.8"}
assert.NoError(t, frm.Validate())
})
t.Run("EmptyMake", func(t *testing.T) {
frm := &Lens{LensMake: "", LensModel: "EF 50mm f/1.8"}
assert.Error(t, frm.Validate())
})
t.Run("EmptyModel", func(t *testing.T) {
frm := &Lens{LensMake: "Canon", LensModel: ""}
assert.Error(t, frm.Validate())
})
}