Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26e6e57949 | ||
|
|
b572966bc4 | ||
|
|
bbfa4f1043 | ||
|
|
9ad4f0b75e | ||
|
|
2093f4ece2 | ||
|
|
7bc7703dc2 | ||
|
|
0ed7452e77 | ||
|
|
366cc19c1b | ||
|
|
c72f30eabb | ||
|
|
235f0dade0 | ||
|
|
9c718cbc5f | ||
|
|
02de92afc7 | ||
|
|
add01455fb | ||
|
|
497f3ce47f | ||
|
|
64188e5713 | ||
|
|
33a6d6da97 | ||
|
|
6410e3be85 | ||
|
|
136d7e8215 | ||
|
|
d3fdd7b0d8 | ||
|
|
329fcc204c | ||
|
|
ba2c7894a8 | ||
|
|
07667c8881 | ||
|
|
68504a70a0 | ||
|
|
32073b3d66 | ||
|
|
d1c1664a38 | ||
|
|
8cbcff98ec | ||
|
|
26f82ad95c | ||
|
|
1b90604b2d | ||
|
|
a03c002df4 | ||
|
|
1ce9415d34 | ||
|
|
cc842901b3 | ||
|
|
23d7e4cdbb | ||
|
|
b48f8fca31 | ||
|
|
a4da93326e | ||
|
|
8bd5c0d163 | ||
|
|
972ab87247 | ||
|
|
d1ea664b41 | ||
|
|
a55fadd53a | ||
|
|
c70a66a1c5 | ||
|
|
8357846818 | ||
|
|
31ea91afb8 | ||
|
|
6616541f65 | ||
|
|
01695822bb | ||
|
|
f0186d1aab | ||
|
|
41a3359085 | ||
|
|
d6cce8df2c | ||
|
|
596861a594 | ||
|
|
385ea800a4 | ||
|
|
27fc339e36 | ||
|
|
61e2173f25 | ||
|
|
13a3fc3d1b | ||
|
|
6a1b515ea9 | ||
|
|
82f27e6b63 | ||
|
|
6ac75031ad | ||
|
|
8b7f1331ee | ||
|
|
099aad8475 | ||
|
|
c1b3fba344 | ||
|
|
d7e9be99f9 | ||
|
|
067a8bcb21 | ||
|
|
f5569a62f1 | ||
|
|
1ab1ed8a82 | ||
|
|
8d95baa70f | ||
|
|
050bce163a | ||
|
|
68b84683d0 | ||
|
|
6fe373d540 | ||
|
|
ba19bcc729 | ||
|
|
f1b65db2c7 | ||
|
|
f5f287ef06 | ||
|
|
f161e6033c | ||
|
|
c86bff8811 | ||
|
|
596b1045bd | ||
|
|
907ca2b590 | ||
|
|
f1b5469b83 | ||
|
|
984fbde540 | ||
|
|
2bdc05dd47 | ||
|
|
d2ea572abf | ||
|
|
8d5c4cd578 | ||
|
|
dd1ba9b518 | ||
|
|
cebe929118 | ||
|
|
3eccf01f1f | ||
|
|
050f1b7941 | ||
|
|
53cc370727 | ||
|
|
363e05d580 | ||
|
|
2ecc80e18c | ||
|
|
0728b28856 | ||
|
|
a6a9270f63 | ||
|
|
372429a9f5 | ||
|
|
4733ad8eb1 | ||
|
|
8e96aa26b4 | ||
|
|
c0e8520d31 | ||
|
|
345f80d90d | ||
|
|
ffcef2fe99 | ||
|
|
e77dc33333 | ||
|
|
5fee19f6c7 | ||
|
|
659c623240 | ||
|
|
695d0ea13a | ||
|
|
0f7ac514fb | ||
|
|
9ad3be2e97 | ||
|
|
2c36fab8aa | ||
|
|
3b7a4b6e52 | ||
|
|
499302c48e | ||
|
|
28fbe76547 | ||
|
|
fabb31275d | ||
|
|
96b75af027 | ||
|
|
dc048ef047 | ||
|
|
d6b2960a4a | ||
|
|
b47b6c4f56 | ||
|
|
75427df7de | ||
|
|
cb98885e1f | ||
|
|
7e0445cc94 | ||
|
|
a9836aaea0 | ||
|
|
27b28dba0d | ||
|
|
86d059bf99 | ||
|
|
76f5ce32ca | ||
|
|
ff9eab998a | ||
|
|
ac3a36eb13 | ||
|
|
4b4c749756 | ||
|
|
4777f4059f | ||
|
|
ad5d555ac8 | ||
|
|
fc7540fc9b | ||
|
|
63a2bdea48 | ||
|
|
41d47fdb3e | ||
|
|
86dae37c5d | ||
|
|
2a97d9379e | ||
|
|
ba0060316c | ||
|
|
41f18a23e5 | ||
|
|
bf26ad4acc | ||
|
|
acc56ad42b | ||
|
|
922e779118 | ||
|
|
f104a6e1b6 | ||
|
|
0aa6052eba | ||
|
|
f25b34d5e2 | ||
|
|
5e5b3e0830 | ||
|
|
f1163139d7 | ||
|
|
5db6e4364a | ||
|
|
3d1a3fd7cf | ||
|
|
6625face50 | ||
|
|
d9764c251d | ||
|
|
243c8356e7 | ||
|
|
2d2f2ab02a | ||
|
|
50785e0577 | ||
|
|
9c1a827685 | ||
|
|
8c658b7eda | ||
|
|
26d8d3daee | ||
|
|
26476db95d | ||
|
|
3c90f6ae61 | ||
|
|
4889afdf0c | ||
|
|
2e777ea752 | ||
|
|
fddf1e21f3 | ||
|
|
d6013b10e6 | ||
|
|
aac6af40cc | ||
|
|
1f96a0e4d3 | ||
|
|
99716984ad | ||
|
|
24e9404a6f | ||
|
|
1f1adba5fd | ||
|
|
b907354746 | ||
|
|
9a08d4a412 | ||
|
|
3a08e6dd55 | ||
|
|
02f782a5ab | ||
|
|
30d68b2d1e | ||
|
|
8974cde582 | ||
|
|
5f3206a4e8 | ||
|
|
40e38d225e | ||
|
|
a73072c8fb | ||
|
|
6a5c5a4c08 | ||
|
|
fa7d5cee87 | ||
|
|
fe4db30e6d |
+17
-3
@@ -1,16 +1,30 @@
|
|||||||
/logs/
|
/logs/
|
||||||
/target/
|
/target/
|
||||||
/var/
|
/var/
|
||||||
/frontend/node_modules/
|
|
||||||
/frontend/public/
|
|
||||||
/.vscode/
|
/.vscode/
|
||||||
/.idea/
|
/.idea/
|
||||||
/bin/
|
/bin/
|
||||||
/.git/
|
/.git/
|
||||||
|
|
||||||
|
# frontend files not needed in docker image
|
||||||
|
/frontend/node_modules/
|
||||||
|
/frontend/apps/remark42/node_modules/
|
||||||
|
/frontend/apps/remark42/public/
|
||||||
|
# e2e tests arficats
|
||||||
|
/frontend/e2e/playwright-report/
|
||||||
|
/frontend/e2e/playwright/.cache/
|
||||||
|
/frontend/e2e/test-results/
|
||||||
|
|
||||||
# source files
|
# source files
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
compose-dev-backend.yml
|
||||||
|
compose-dev-frontend.yml
|
||||||
|
compose-private-backend.yml
|
||||||
|
compose-private-frontend.yml
|
||||||
|
compose-e2e-test.yml
|
||||||
|
compose-private.yml
|
||||||
rest-client.env.json
|
rest-client.env.json
|
||||||
|
Makefile
|
||||||
|
|
||||||
# generated files
|
# generated files
|
||||||
*.cov
|
*.cov
|
||||||
@@ -22,4 +36,4 @@ debug.test
|
|||||||
*.test
|
*.test
|
||||||
remark42
|
remark42
|
||||||
/backend/var/
|
/backend/var/
|
||||||
compose-private-backend.yml
|
/playwright-report/
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|||||||
+2
-1
@@ -2,4 +2,5 @@
|
|||||||
# Unless a later match takes precedence, @umputun will be requested for
|
# Unless a later match takes precedence, @umputun will be requested for
|
||||||
# review when someone opens a pull request.
|
# review when someone opens a pull request.
|
||||||
|
|
||||||
* @umputun
|
* @umputun
|
||||||
|
frontend/* @akellbl4 @Mavrin
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: test_backend
|
name: backend
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -10,17 +10,19 @@ on:
|
|||||||
- "!backend/scripts/**"
|
- "!backend/scripts/**"
|
||||||
- "!**.md"
|
- "!**.md"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
types: [opened, reopened]
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/ci-test-backend.yml"
|
- ".github/workflows/ci-test-backend.yml"
|
||||||
- "backend/**"
|
- "backend/**"
|
||||||
- "!backend/scripts/**"
|
- "!backend/scripts/**"
|
||||||
- "!**.md"
|
- "!**.md"
|
||||||
jobs:
|
jobs:
|
||||||
test-backend:
|
test:
|
||||||
|
name: Test & Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: debug if needed
|
- name: debug if needed
|
||||||
run: if [[ "$DEBUG" == "true" ]]; then env; fi
|
run: if [[ "$DEBUG" == "true" ]]; then env; fi
|
||||||
@@ -28,37 +30,44 @@ jobs:
|
|||||||
DEBUG: ${{secrets.DEBUG}}
|
DEBUG: ${{secrets.DEBUG}}
|
||||||
|
|
||||||
- name: install go
|
- name: install go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: "1.20"
|
||||||
|
|
||||||
- name: install golangci-lint and goveralls
|
- name: test and build backend
|
||||||
run: |
|
|
||||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.44.0
|
|
||||||
go get -u github.com/mattn/goveralls
|
|
||||||
|
|
||||||
- name: test and lint backend
|
|
||||||
run: |
|
run: |
|
||||||
go test -race -timeout=60s -covermode=atomic -coverprofile=$GITHUB_WORKSPACE/profile.cov_tmp ./...
|
go test -race -timeout=60s -covermode=atomic -coverprofile=$GITHUB_WORKSPACE/profile.cov_tmp ./...
|
||||||
cat $GITHUB_WORKSPACE/profile.cov_tmp | grep -v "_mock.go" > $GITHUB_WORKSPACE/profile.cov
|
cat $GITHUB_WORKSPACE/profile.cov_tmp | grep -v "_mock.go" > $GITHUB_WORKSPACE/profile.cov
|
||||||
$GITHUB_WORKSPACE/golangci-lint --config ${GITHUB_WORKSPACE}/backend/.golangci.yml run --out-format=github-actions ./...
|
go build -race ./...
|
||||||
working-directory: backend/app
|
working-directory: backend/app
|
||||||
env:
|
env:
|
||||||
GOFLAGS: "-mod=vendor"
|
|
||||||
TZ: "America/Chicago"
|
TZ: "America/Chicago"
|
||||||
|
|
||||||
- name: test and lint examples
|
- name: test examples
|
||||||
run: |
|
run: |
|
||||||
go version
|
|
||||||
$GITHUB_WORKSPACE/golangci-lint version
|
|
||||||
go test -race ./...
|
go test -race ./...
|
||||||
$GITHUB_WORKSPACE/golangci-lint --config ${GITHUB_WORKSPACE}/backend/.golangci.yml run --out-format=github-actions ./...
|
go build -race ./...
|
||||||
working-directory: backend/_example/memory_store
|
working-directory: backend/_example/memory_store
|
||||||
env:
|
env:
|
||||||
TZ: "America/Chicago"
|
TZ: "America/Chicago"
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
working-directory: backend/app
|
||||||
|
|
||||||
|
- name: golangci-lint on example directory
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
args: --config ../../.golangci.yml
|
||||||
|
working-directory: backend/_example/memory_store
|
||||||
|
|
||||||
- name: submit coverage
|
- name: submit coverage
|
||||||
run: $(go env GOPATH)/bin/goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov
|
run: |
|
||||||
|
go install github.com/mattn/goveralls@latest
|
||||||
|
goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
env:
|
env:
|
||||||
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -7,16 +7,17 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- ".github/workflows/ci-build.yml"
|
- ".github/workflows/ci-build.yml"
|
||||||
- "backend/**"
|
- "backend/**"
|
||||||
- "frontend/**"
|
- "frontend/apps/**"
|
||||||
- ".dockerignore"
|
- ".dockerignore"
|
||||||
- "docker-init.sh"
|
- "docker-init.sh"
|
||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
- "!**.md"
|
- "!**.md"
|
||||||
|
- "!frontend/packages/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/ci-build.yml"
|
- ".github/workflows/ci-build.yml"
|
||||||
- "backend/**"
|
- "backend/**"
|
||||||
- "frontend/**"
|
- "frontend/apps/**"
|
||||||
- ".dockerignore"
|
- ".dockerignore"
|
||||||
- "docker-init.sh"
|
- "docker-init.sh"
|
||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
@@ -24,21 +25,36 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-images:
|
build-images:
|
||||||
|
name: Build Docker images
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: set up QEMU
|
- name: set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: set up Docker Buildx
|
- name: set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: available platforms
|
- name: available platforms
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
|
|
||||||
|
- name: build docker image without pushing (only outside master)
|
||||||
|
if: ${{ github.ref != 'refs/heads/master' }}
|
||||||
|
run: |
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true \
|
||||||
|
--platform linux/amd64 .
|
||||||
|
|
||||||
|
- name: build example docker image without pushing (only outside master)
|
||||||
|
if: ${{ github.ref != 'refs/heads/master' }}
|
||||||
|
run: |
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true \
|
||||||
|
--platform linux/amd64 -f backend/_example/memory_store/Dockerfile .
|
||||||
|
|
||||||
- name: build and deploy master image to ghcr.io and dockerhub
|
- name: build and deploy master image to ghcr.io and dockerhub
|
||||||
if: ${{ github.ref == 'refs/heads/master' }}
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
env:
|
env:
|
||||||
@@ -51,12 +67,12 @@ jobs:
|
|||||||
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
|
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
|
||||||
echo "GITHUB_REF=${GITHUB_REF}, GITHUB_SHA=${GITHUB_SHA}, GIT_BRANCH=${ref}"
|
echo "GITHUB_REF=${GITHUB_REF}, GITHUB_SHA=${GITHUB_SHA}, GIT_BRANCH=${ref}"
|
||||||
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
||||||
echo ${DOCKER_HUB_TOKEN} | docker login -u ${USERNAME} --password-stdin
|
echo ${DOCKER_HUB_TOKEN} | docker login -u umputun --password-stdin
|
||||||
docker buildx build --push \
|
docker buildx build --push \
|
||||||
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true --build-arg CI=github \
|
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true --build-arg CI=github \
|
||||||
--build-arg GITHUB_SHA=${GITHUB_SHA} --build-arg GIT_BRANCH=${ref} --build-arg GITHUB_REF=${GITHUB_REF} \
|
--build-arg GITHUB_SHA=${GITHUB_SHA} --build-arg GIT_BRANCH=${ref} --build-arg GITHUB_REF=${GITHUB_REF} \
|
||||||
--platform linux/amd64,linux/arm/v7,linux/arm64 \
|
--platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||||
-t ghcr.io/${USERNAME}/remark42:${ref} -t ${USERNAME}/remark42:${ref} .
|
-t ghcr.io/umputun/remark42:${ref} -t umputun/remark42:${ref} .
|
||||||
|
|
||||||
- name: deploy tagged (latest) to ghcr.io and dockerhub
|
- name: deploy tagged (latest) to ghcr.io and dockerhub
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
@@ -70,7 +86,7 @@ jobs:
|
|||||||
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
|
ref="$(echo ${GITHUB_REF} | cut -d'/' -f3)"
|
||||||
echo "GITHUB_REF=${GITHUB_REF}, GITHUB_SHA=${GITHUB_SHA}, GIT_BRANCH=${ref}"
|
echo "GITHUB_REF=${GITHUB_REF}, GITHUB_SHA=${GITHUB_SHA}, GIT_BRANCH=${ref}"
|
||||||
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
||||||
echo ${DOCKER_HUB_TOKEN} | docker login -u ${USERNAME} --password-stdin
|
echo ${DOCKER_HUB_TOKEN} | docker login -u umputun --password-stdin
|
||||||
docker buildx build --push \
|
docker buildx build --push \
|
||||||
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true --build-arg CI=github \
|
--build-arg SKIP_BACKEND_TEST=true --build-arg SKIP_FRONTEND_TEST=true --build-arg CI=github \
|
||||||
--build-arg GITHUB_SHA=${GITHUB_SHA} --build-arg GIT_BRANCH=${ref} --build-arg GITHUB_REF=${GITHUB_REF} \
|
--build-arg GITHUB_SHA=${GITHUB_SHA} --build-arg GIT_BRANCH=${ref} --build-arg GITHUB_REF=${GITHUB_REF} \
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
name: "@remark42/api"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/ci-frontend-api.yml"
|
||||||
|
- "frontend/packages/**"
|
||||||
|
- "!**.md"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/ci-frontend-api.yml"
|
||||||
|
- "frontend/packages/**"
|
||||||
|
- "!**.md"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
type-check:
|
||||||
|
name: Type check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: [16.15.1]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2.0.1
|
||||||
|
id: pnpm-install
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- name: Run type check
|
||||||
|
run: pnpm type-check:api
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: [16.15.1]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2.0.1
|
||||||
|
id: pnpm-install
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- name: Run linters
|
||||||
|
run: pnpm lint:api
|
||||||
|
working-directory: ./frontend/
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Tests & Coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: [16.15.1]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2.0.1
|
||||||
|
id: pnpm-install
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- name: Test & Coverage
|
||||||
|
run: pnpm coverage:api
|
||||||
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- name: Submit coverage
|
||||||
|
run: ${{ github.workspace }}/frontend/apps/remark42/node_modules/.bin/codecov
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
name: frontend
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- ".github/workflows/ci-frontend-size-limit.yml"
|
|
||||||
- "frontend/**"
|
|
||||||
- "!**.md"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
size:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
CI_JOB_NUMBER: 1
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- uses: andresz1/size-limit-action@v1
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
directory: frontend
|
|
||||||
@@ -3,105 +3,219 @@ name: frontend
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
tags:
|
- master
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/ci-frontend.yml"
|
- ".github/workflows/ci-frontend.yml"
|
||||||
- "frontend/**"
|
- "frontend/apps/remark42/**"
|
||||||
- "!**.md"
|
- "!**.md"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/ci-frontend.yml"
|
- ".github/workflows/ci-frontend.yml"
|
||||||
- "frontend/**"
|
- "frontend/apps/remark42/**"
|
||||||
- "!**.md"
|
- "!**.md"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-translations:
|
translations-check:
|
||||||
|
name: Translations check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [16.13.2]
|
node: [16.15.1]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout
|
||||||
- uses: actions/setup-node@v1
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
- run: npm ci --loglevel warn
|
- name: Install pnpm
|
||||||
working-directory: ./frontend
|
uses: pnpm/action-setup@v2.0.1
|
||||||
|
id: pnpm-install
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/frontend/node_modules/.cache
|
version: 7
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
run_install: false
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
|
|
||||||
- run: npm run check:translation
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
|
||||||
check-typescript:
|
- name: Translations check
|
||||||
|
run: pnpm translation-check
|
||||||
|
working-directory: ./frontend/apps/remark42
|
||||||
|
|
||||||
|
type-check:
|
||||||
|
name: Type check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [16.13.2]
|
node: [16.15.1]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout
|
||||||
- uses: actions/setup-node@v1
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
- run: npm ci --loglevel warn
|
- name: Install pnpm
|
||||||
working-directory: ./frontend
|
uses: pnpm/action-setup@v2.0.1
|
||||||
|
id: pnpm-install
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/frontend/node_modules/.cache
|
version: 7
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
run_install: false
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
|
|
||||||
- run: npm run check:types
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
|
||||||
|
- name: Run type check
|
||||||
|
run: pnpm type-check
|
||||||
|
working-directory: ./frontend/apps/remark42
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
|
name: Eslint & Stylelint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [16.13.2]
|
node: [16.15.1]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout
|
||||||
- uses: actions/setup-node@v1
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
- run: npm ci --loglevel warn
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2.0.1
|
||||||
|
id: pnpm-install
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
|
||||||
- run: npm run lint
|
- name: Run linters
|
||||||
working-directory: ./frontend
|
run: pnpm lint
|
||||||
|
working-directory: ./frontend/apps/remark42
|
||||||
|
|
||||||
|
size-limit:
|
||||||
|
name: Size limit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
CI_JOB_NUMBER: 1
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2.0.1
|
||||||
|
id: pnpm-install
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Check bundle size
|
||||||
|
uses: andresz1/size-limit-action@dd31dce7dcc72a041fd3e49abf0502b13fc4ce05
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
directory: ./frontend/apps/remark42
|
||||||
|
package_manager: pnpm
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
name: Tests & Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [16.13.2]
|
node: [16.15.1]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout
|
||||||
- uses: actions/setup-node@v1
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
- run: npm ci --loglevel warn
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2.0.1
|
||||||
|
id: pnpm-install
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-cache
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
|
||||||
- run: npm run test:coverage
|
- name: Test & Coverage
|
||||||
working-directory: ./frontend
|
run: pnpm coverage
|
||||||
|
working-directory: ./frontend/apps/remark42
|
||||||
|
|
||||||
- name: submit coverage
|
- name: Submit coverage
|
||||||
run: node ${{ github.workspace }}/frontend/node_modules/.bin/codecov
|
run: ${{ github.workspace }}/frontend/apps/remark42/node_modules/.bin/codecov
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name: site
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
- master
|
||||||
tags:
|
tags:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/ci-site.yml"
|
- ".github/workflows/ci-site.yml"
|
||||||
@@ -13,20 +14,20 @@ on:
|
|||||||
- "site/**"
|
- "site/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build:
|
||||||
build-site:
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: set up QEMU
|
- name: set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: set up Docker Buildx
|
- name: set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: available platforms
|
- name: available platforms
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
@@ -44,7 +45,7 @@ jobs:
|
|||||||
echo GITHUB_REF - $ref
|
echo GITHUB_REF - $ref
|
||||||
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
||||||
docker buildx build --push --no-cache --platform linux/amd64,linux/arm/v7,linux/arm64 \
|
docker buildx build --push --no-cache --platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||||
-t ghcr.io/${USERNAME}/remark24-site:${ref} .
|
-t ghcr.io/umputun/remark42-site:${ref} .
|
||||||
|
|
||||||
- name: deploy tagged (latest) to ghcr.io and dockerhub
|
- name: deploy tagged (latest) to ghcr.io and dockerhub
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
@@ -59,7 +60,7 @@ jobs:
|
|||||||
echo "GITHUB_REF=$ref, GITHUB_SHA=${GITHUB_SHA}"
|
echo "GITHUB_REF=$ref, GITHUB_SHA=${GITHUB_SHA}"
|
||||||
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
echo ${GITHUB_PACKAGE_TOKEN} | docker login ghcr.io -u ${USERNAME} --password-stdin
|
||||||
docker buildx build --push --no-cache --platform linux/amd64,linux/arm/v7,linux/arm64 \
|
docker buildx build --push --no-cache --platform linux/amd64,linux/arm/v7,linux/arm64 \
|
||||||
-t ghcr.io/${USERNAME}/remark24-site:${ref} -t ghcr.io/${USERNAME}/remark24-site:latest .
|
-t ghcr.io/umputun/remark42-site:${ref} -t ghcr.io/umputun/remark42-site:latest .
|
||||||
|
|
||||||
- name: remote site deployment from master
|
- name: remote site deployment from master
|
||||||
if: ${{ github.ref == 'refs/heads/master' }}
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: e2e
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- "frontend/apps/remark42/**"
|
||||||
|
- "frontend/e2e/**"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- "frontend/apps/remark42/**"
|
||||||
|
- "frontend/e2e/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build & run containers
|
||||||
|
id: tests
|
||||||
|
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f compose-e2e-test.yml up --build --quiet-pull --exit-code-from tests
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: ./playwright-report/
|
||||||
|
retention-days: 30
|
||||||
+3
-2
@@ -4,7 +4,6 @@ target
|
|||||||
/logs/
|
/logs/
|
||||||
/target/
|
/target/
|
||||||
/var/
|
/var/
|
||||||
/web/
|
|
||||||
debug
|
debug
|
||||||
debug.test
|
debug.test
|
||||||
.vscode
|
.vscode
|
||||||
@@ -18,10 +17,12 @@ remark42
|
|||||||
/bin/
|
/bin/
|
||||||
/backend/var/
|
/backend/var/
|
||||||
/backend/app/var/
|
/backend/app/var/
|
||||||
/backend/web/
|
/backend/app/cmd/web/
|
||||||
/backend/*.html.tmpl
|
/backend/*.html.tmpl
|
||||||
compose-private-backend.yml
|
compose-private-backend.yml
|
||||||
compose-private-frontend.yml
|
compose-private-frontend.yml
|
||||||
compose-private.yml
|
compose-private.yml
|
||||||
/backend/_example/*/vendor
|
/backend/_example/*/vendor
|
||||||
http-client.env.json
|
http-client.env.json
|
||||||
|
/playwright-report/
|
||||||
|
/backend/app/cmd/var
|
||||||
|
|||||||
+66
-50
@@ -1,4 +1,51 @@
|
|||||||
FROM umputun/baseimage:buildgo-v1.9.1 as build-backend
|
FROM --platform=$BUILDPLATFORM node:16.15.1-alpine AS frontend-deps
|
||||||
|
|
||||||
|
ARG SKIP_FRONTEND_TEST
|
||||||
|
ARG SKIP_FRONTEND_BUILD
|
||||||
|
|
||||||
|
WORKDIR /srv/frontend/
|
||||||
|
|
||||||
|
COPY ./frontend/package.json ./frontend/pnpm-lock.yaml ./frontend/pnpm-workspace.yaml /srv/frontend/
|
||||||
|
COPY ./frontend/apps/remark42/package.json /srv/frontend/apps/remark42/
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
if [[ -z "$SKIP_FRONTEND_BUILD" || -z "$SKIP_FRONTEND_TEST" ]]; then \
|
||||||
|
apk add --no-cache --update git && \
|
||||||
|
npm i -g pnpm@7; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN --mount=type=cache,id=pnpm,target=/root/.pnpm-store/v3 \
|
||||||
|
if [[ -z "$SKIP_FRONTEND_BUILD" || -z "$SKIP_FRONTEND_TEST" ]]; then \
|
||||||
|
pnpm i; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
FROM --platform=$BUILDPLATFORM frontend-deps AS build-frontend
|
||||||
|
|
||||||
|
ARG SKIP_FRONTEND_TEST
|
||||||
|
ARG SKIP_FRONTEND_BUILD
|
||||||
|
ENV CI=true
|
||||||
|
|
||||||
|
WORKDIR /srv/frontend/apps/remark42/
|
||||||
|
|
||||||
|
COPY ./frontend/apps/remark42/ /srv/frontend/apps/remark42/
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
if [ -z "$SKIP_FRONTEND_TEST" ]; then \
|
||||||
|
pnpm lint type-check translation-check test; \
|
||||||
|
else \
|
||||||
|
echo 'Skip frontend test'; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
if [ -z "$SKIP_FRONTEND_BUILD" ]; then \
|
||||||
|
pnpm build; \
|
||||||
|
else \
|
||||||
|
mkdir /srv/frontend/apps/remark42/public; \
|
||||||
|
echo 'Skip frontend build'; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
FROM umputun/baseimage:buildgo-v1.11.0 as build-backend
|
||||||
|
|
||||||
ARG CI
|
ARG CI
|
||||||
ARG GITHUB_REF
|
ARG GITHUB_REF
|
||||||
@@ -8,10 +55,11 @@ ARG SKIP_BACKEND_TEST
|
|||||||
ARG BACKEND_TEST_TIMEOUT
|
ARG BACKEND_TEST_TIMEOUT
|
||||||
|
|
||||||
ADD backend /build/backend
|
ADD backend /build/backend
|
||||||
|
# to embed the frontend files statically into Remark42 binary
|
||||||
|
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /build/backend/app/cmd/web/
|
||||||
|
RUN find /build/backend/app/cmd/web/ -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \;
|
||||||
WORKDIR /build/backend
|
WORKDIR /build/backend
|
||||||
|
|
||||||
ENV GOFLAGS="-mod=vendor"
|
|
||||||
|
|
||||||
# install gcc in order to be able to go test package with -race
|
# install gcc in order to be able to go test package with -race
|
||||||
RUN apk --no-cache add gcc libc-dev
|
RUN apk --no-cache add gcc libc-dev
|
||||||
|
|
||||||
@@ -22,10 +70,10 @@ RUN \
|
|||||||
cd app && \
|
cd app && \
|
||||||
if [ -z "$SKIP_BACKEND_TEST" ] ; then \
|
if [ -z "$SKIP_BACKEND_TEST" ] ; then \
|
||||||
CGO_ENABLED=1 go test -race -p 1 -timeout="${BACKEND_TEST_TIMEOUT:-300s}" -covermode=atomic -coverprofile=/profile.cov_tmp ./... && \
|
CGO_ENABLED=1 go test -race -p 1 -timeout="${BACKEND_TEST_TIMEOUT:-300s}" -covermode=atomic -coverprofile=/profile.cov_tmp ./... && \
|
||||||
cat /profile.cov_tmp | grep -v "_mock.go" > /profile.cov ; \
|
cat /profile.cov_tmp | grep -v "_mock.go" > /profile.cov && \
|
||||||
golangci-lint run --config ../.golangci.yml ./... ; \
|
golangci-lint run --config ../.golangci.yml ./... ; \
|
||||||
else \
|
else \
|
||||||
echo "skip backend tests and linter" \
|
echo "skip backend tests and linter" \
|
||||||
; fi
|
; fi
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
@@ -33,65 +81,33 @@ RUN \
|
|||||||
echo "version=$version" && \
|
echo "version=$version" && \
|
||||||
go build -o remark42 -ldflags "-X main.revision=${version} -s -w" ./app
|
go build -o remark42 -ldflags "-X main.revision=${version} -s -w" ./app
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM node:16.13.2-alpine as build-frontend-deps
|
FROM umputun/baseimage:app-v1.11.0
|
||||||
|
|
||||||
ARG CI
|
ARG GITHUB_SHA
|
||||||
ARG SKIP_FRONTEND_BUILD
|
|
||||||
ENV HUSKY_SKIP_INSTALL=true
|
|
||||||
|
|
||||||
RUN if [ -z "$SKIP_FRONTEND_BUILD" ] ; then \
|
LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
|
||||||
apk add --no-cache --update git \
|
org.opencontainers.image.description="Remark42 comment engine" \
|
||||||
; fi
|
org.opencontainers.image.documentation="https://remark42.com/docs/getting-started/" \
|
||||||
ADD frontend/package.json /srv/frontend/package.json
|
org.opencontainers.image.licenses="MIT" \
|
||||||
ADD frontend/package-lock.json /srv/frontend/package-lock.json
|
org.opencontainers.image.source="https://github.com/umputun/remark42.git" \
|
||||||
WORKDIR /srv/frontend
|
org.opencontainers.image.title="Remark42" \
|
||||||
RUN mkdir node_modules
|
org.opencontainers.image.url="https://remark42.com/" \
|
||||||
RUN if [ -z "$SKIP_FRONTEND_BUILD" ] ; then \
|
org.opencontainers.image.revision="${GITHUB_SHA}"
|
||||||
CI=true npm ci --loglevel warn \
|
|
||||||
else \
|
|
||||||
echo "skip frontend build" \
|
|
||||||
; fi
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM node:16.13.2-alpine as build-frontend
|
|
||||||
|
|
||||||
ARG CI
|
|
||||||
ARG SKIP_FRONTEND_TEST
|
|
||||||
ARG SKIP_FRONTEND_BUILD
|
|
||||||
ARG NODE_ENV=production
|
|
||||||
|
|
||||||
COPY --from=build-frontend-deps /srv/frontend/node_modules /srv/frontend/node_modules
|
|
||||||
ADD frontend /srv/frontend
|
|
||||||
WORKDIR /srv/frontend
|
|
||||||
RUN mkdir public
|
|
||||||
RUN if [ -z "$SKIP_FRONTEND_BUILD" ] ; then \
|
|
||||||
if [ -z "$SKIP_FRONTEND_TEST" ] ; then \
|
|
||||||
npm run lint test check; \
|
|
||||||
else \
|
|
||||||
echo "skip frontend tests and lint" ; npm run build \
|
|
||||||
; fi \
|
|
||||||
; fi
|
|
||||||
RUN rm -rf ./node_modules
|
|
||||||
|
|
||||||
FROM umputun/baseimage:app-v1.9.1
|
|
||||||
|
|
||||||
WORKDIR /srv
|
WORKDIR /srv
|
||||||
|
|
||||||
ADD docker-init.sh /entrypoint.sh
|
COPY docker-init.sh /srv/init.sh
|
||||||
ADD backend/scripts/backup.sh /usr/local/bin/backup
|
ADD backend/scripts/backup.sh /usr/local/bin/backup
|
||||||
ADD backend/scripts/restore.sh /usr/local/bin/restore
|
ADD backend/scripts/restore.sh /usr/local/bin/restore
|
||||||
ADD backend/scripts/import.sh /usr/local/bin/import
|
ADD backend/scripts/import.sh /usr/local/bin/import
|
||||||
RUN chmod +x /entrypoint.sh /usr/local/bin/backup /usr/local/bin/restore /usr/local/bin/import
|
RUN chmod +x /srv/init.sh /usr/local/bin/backup /usr/local/bin/restore /usr/local/bin/import
|
||||||
|
|
||||||
COPY --from=build-backend /build/backend/remark42 /srv/remark42
|
COPY --from=build-backend /build/backend/remark42 /srv/remark42
|
||||||
COPY --from=build-backend /build/backend/templates /srv
|
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /srv/web/
|
||||||
COPY --from=build-frontend /srv/frontend/public/ /srv/web
|
|
||||||
COPY docker-init.sh /srv/init.sh
|
|
||||||
RUN chown -R app:app /srv
|
RUN chown -R app:app /srv
|
||||||
RUN ln -s /srv/remark42 /usr/bin/remark42
|
RUN ln -s /srv/remark42 /usr/bin/remark42
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1
|
||||||
|
|
||||||
|
|
||||||
RUN chmod +x /srv/init.sh
|
|
||||||
CMD ["/srv/remark42", "server"]
|
CMD ["/srv/remark42", "server"]
|
||||||
|
|||||||
+19
-31
@@ -1,53 +1,41 @@
|
|||||||
FROM node:12.16-alpine as build-frontend-deps
|
FROM node:16.15.1-alpine AS frontend-deps
|
||||||
|
|
||||||
ARG CI
|
ENV CI=true
|
||||||
|
|
||||||
ENV SKIP_FRONTEND_TEST=true
|
WORKDIR /srv/frontend
|
||||||
|
|
||||||
RUN apk add --no-cache --update git
|
COPY ./frontend/package.json ./frontend/pnpm-lock.yaml ./frontend/pnpm-workspace.yaml /srv/frontend/
|
||||||
ADD frontend/package.json /srv/frontend/package.json
|
COPY ./frontend/apps/remark42/package.json /srv/frontend/apps/remark42/package.json
|
||||||
ADD frontend/package-lock.json /srv/frontend/package-lock.json
|
|
||||||
RUN cd /srv/frontend && CI=true npm ci
|
|
||||||
|
|
||||||
FROM node:12.16-alpine as build-frontend
|
RUN apk add --no-cache --update git && npm i -g pnpm@7
|
||||||
|
RUN --mount=type=cache,id=pnpm,target=/root/.pnpm-store/v3 pnpm i
|
||||||
|
|
||||||
ARG CI
|
FROM frontend-deps AS build-frontend
|
||||||
ARG NODE_ENV=production
|
|
||||||
ENV SKIP_FRONTEND_TEST=true
|
|
||||||
ENV HUSKY_SKIP_INSTALL=true
|
|
||||||
|
|
||||||
COPY --from=build-frontend-deps /srv/frontend/node_modules /srv/frontend/node_modules
|
ENV NODE_ENV=production
|
||||||
ADD frontend /srv/frontend
|
ENV CI=true
|
||||||
RUN cd /srv/frontend && \
|
|
||||||
npm run build && \
|
|
||||||
rm -rf ./node_modules
|
|
||||||
|
|
||||||
FROM umputun/baseimage:buildgo-v1.9.1 as build-backend
|
WORKDIR /srv/frontend/apps/remark42/
|
||||||
|
COPY ./frontend/apps/remark42/ /srv/frontend/apps/remark42/
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM umputun/baseimage:buildgo-v1.9.2 as build-backend
|
||||||
|
|
||||||
ARG GITHUB_TOKEN
|
ARG GITHUB_TOKEN
|
||||||
ARG GITHUB_REF
|
ARG GITHUB_REF
|
||||||
ARG GITHUB_SHA
|
ARG GITHUB_SHA
|
||||||
ENV SKIP_BACKEND_TEST=true
|
|
||||||
|
|
||||||
WORKDIR /build/backend
|
WORKDIR /build/backend
|
||||||
ADD backend /build/backend
|
ADD backend /build/backend
|
||||||
ADD README.md /build/
|
ADD README.md /build/
|
||||||
ADD LICENSE /build/
|
ADD LICENSE /build/
|
||||||
|
|
||||||
COPY --from=build-frontend /srv/frontend/public/ web
|
COPY --from=build-frontend /srv/frontend/apps/remark42/public/ /build/backend/app/cmd/web/
|
||||||
|
|
||||||
|
RUN find /build/backend/app/cmd/web/ -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \;
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
export WEB_ROOT=/build/backend/web && \
|
version=$("/script/version.sh") && echo "version=${version}" && \
|
||||||
find . -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \; && \
|
|
||||||
statik --src=${WEB_ROOT} --dest=/build/backend/app/rest -p api -f && \
|
|
||||||
statik --src=/build/backend/templates --dest=/build/backend/app -p templates -ns templates -f && \
|
|
||||||
ls -la /build/backend/app/templates/statik.go && \
|
|
||||||
ls -la /build/backend/app/rest/api/statik.go && \
|
|
||||||
ls -la /build/backend/web/
|
|
||||||
|
|
||||||
RUN \
|
|
||||||
version=$("/script/version.sh") && echo "version=${version}" && \
|
|
||||||
export GOFLAGS="-mod=vendor" && \
|
|
||||||
GOOS=linux GOARCH=amd64 go build -o remark42.linux-amd64 -ldflags "-X main.revision=${version} -s -w" ./app && \
|
GOOS=linux GOARCH=amd64 go build -o remark42.linux-amd64 -ldflags "-X main.revision=${version} -s -w" ./app && \
|
||||||
GOOS=linux GOARCH=386 go build -o remark42.linux-386 -ldflags "-X main.revision=${version} -s -w" ./app && \
|
GOOS=linux GOARCH=386 go build -o remark42.linux-386 -ldflags "-X main.revision=${version} -s -w" ./app && \
|
||||||
GOOS=linux GOARCH=arm go build -o remark42.linux-arm -ldflags "-X main.revision=${version} -s -w" ./app && \
|
GOOS=linux GOARCH=arm go build -o remark42.linux-arm -ldflags "-X main.revision=${version} -s -w" ./app && \
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ bin:
|
|||||||
docker rm -f remark42.bin
|
docker rm -f remark42.bin
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
docker build -t umputun/remark42 --build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) \
|
DOCKER_BUILDKIT=1 docker build -t umputun/remark42 --build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) \
|
||||||
--build-arg CI=true --build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true .
|
--build-arg CI=true --build-arg SKIP_FRONTEND_TEST=true --build-arg SKIP_BACKEND_TEST=true .
|
||||||
|
|
||||||
dockerx:
|
dockerx:
|
||||||
@@ -22,7 +22,7 @@ dockerx:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
docker build -f Dockerfile.artifacts --no-cache --pull --build-arg CI=true \
|
docker build -f Dockerfile.artifacts --no-cache --pull --build-arg CI=true \
|
||||||
--build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) -t remark42.bin .
|
--build-arg GITHUB_REF=$(GITHUB_REF) --build-arg GITHUB_SHA=$(GITHUB_SHA) -t remark42.bin .
|
||||||
- @docker rm -f remark42.bin 2>/dev/null || exit 0
|
- @docker rm -f remark42.bin 2>/dev/null || exit 0
|
||||||
- @mkdir -p bin
|
- @mkdir -p bin
|
||||||
docker run -d --name=remark42.bin remark42.bin
|
docker run -d --name=remark42.bin remark42.bin
|
||||||
@@ -36,7 +36,7 @@ release:
|
|||||||
docker rm -f remark42.bin
|
docker rm -f remark42.bin
|
||||||
|
|
||||||
race_test:
|
race_test:
|
||||||
cd backend/app && go test -race -mod=vendor -timeout=60s -count 1 ./...
|
cd backend/app && go test -race -timeout=60s -count 1 ./...
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
docker-compose -f compose-dev-backend.yml build
|
docker-compose -f compose-dev-backend.yml build
|
||||||
@@ -45,9 +45,11 @@ frontend:
|
|||||||
docker-compose -f compose-dev-frontend.yml build
|
docker-compose -f compose-dev-frontend.yml build
|
||||||
|
|
||||||
rundev:
|
rundev:
|
||||||
docker pull umputun/baseimage:buildgo-v1.9.1
|
|
||||||
SKIP_BACKEND_TEST=true SKIP_FRONTEND_TEST=true GITHUB_REF=$(GITHUB_REF) GITHUB_SHA=$(GITHUB_SHA) CI=true \
|
SKIP_BACKEND_TEST=true SKIP_FRONTEND_TEST=true GITHUB_REF=$(GITHUB_REF) GITHUB_SHA=$(GITHUB_SHA) CI=true \
|
||||||
docker-compose -f compose-private.yml build
|
docker-compose -f compose-private.yml build
|
||||||
docker-compose -f compose-private.yml up
|
docker-compose -f compose-private.yml up
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
docker compose -f compose-e2e-test.yml up --build --quiet-pull --exit-code-from tests
|
||||||
|
|
||||||
.PHONY: bin backend
|
.PHONY: bin backend
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Remark42 [](https://github.com/umputun/remark42/actions) [](https://goreportcard.com/report/github.com/umputun/remark42) [](https://coveralls.io/github/umputun/remark42?branch=master) [](https://app.codecov.io/gh/umputun/remark42)
|
# Remark42 [](https://github.com/umputun/remark42/actions) [](https://hub.docker.com/r/umputun/remark42) [](https://goreportcard.com/report/github.com/umputun/remark42) [](https://coveralls.io/github/umputun/remark42?branch=master) [](https://app.codecov.io/gh/umputun/remark42)
|
||||||
|
|
||||||
Remark42 is a self-hosted, lightweight and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles, or any other place where readers add comments.
|
Remark42 is a self-hosted, lightweight and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles, or any other place where readers add comments.
|
||||||
|
|
||||||
@@ -37,6 +37,12 @@ For admin screenshots see [Admin UI documentation](https://remark42.com/docs/man
|
|||||||
|
|
||||||
All remark42 documentation is available [by the link](https://remark42.com/docs/getting-started/installation/).
|
All remark42 documentation is available [by the link](https://remark42.com/docs/getting-started/installation/).
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
In order to start and work on the project locally in development mode check our contribution documentation for [backend](https://remark42.com/docs/contributing/backend/) and [frontend](https://remark42.com/docs/contributing/frontend/).
|
||||||
|
|
||||||
|
If you are interested in adding a new localization please check [these docs](https://remark42.com/docs/contributing/translations/).
|
||||||
|
|
||||||
## Related projects
|
## Related projects
|
||||||
|
|
||||||
* [A Helm chart for Remark42 on Kubernetes](https://github.com/groundhog2k/helm-charts/tree/master/charts/remark42)
|
* [A Helm chart for Remark42 on Kubernetes](https://github.com/groundhog2k/helm-charts/tree/master/charts/remark42)
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ run:
|
|||||||
linters-settings:
|
linters-settings:
|
||||||
govet:
|
govet:
|
||||||
check-shadowing: true
|
check-shadowing: true
|
||||||
golint:
|
|
||||||
min-confidence: 0.1
|
|
||||||
maligned:
|
maligned:
|
||||||
suggest-new: true
|
suggest-new: true
|
||||||
goconst:
|
goconst:
|
||||||
@@ -37,18 +35,14 @@ linters:
|
|||||||
- revive
|
- revive
|
||||||
- govet
|
- govet
|
||||||
- unconvert
|
- unconvert
|
||||||
- megacheck
|
|
||||||
- structcheck
|
|
||||||
- gas
|
- gas
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- dupl
|
- dupl
|
||||||
- misspell
|
- misspell
|
||||||
- unparam
|
- unparam
|
||||||
- varcheck
|
- unused
|
||||||
- deadcode
|
|
||||||
- typecheck
|
- typecheck
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- varcheck
|
|
||||||
- stylecheck
|
- stylecheck
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- exportloopref
|
- exportloopref
|
||||||
@@ -64,9 +58,9 @@ issues:
|
|||||||
- text: "at least one file in a package should have a package comment"
|
- text: "at least one file in a package should have a package comment"
|
||||||
linters:
|
linters:
|
||||||
- stylecheck
|
- stylecheck
|
||||||
- text: "should have a package comment, unless it's in another file for this package"
|
- text: "package-comments: should have a package comment"
|
||||||
linters:
|
linters:
|
||||||
- golint
|
- revive
|
||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../site/src/docs/contributing/backend/index.md
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
FROM umputun/baseimage:buildgo-latest as build-backend
|
FROM umputun/baseimage:buildgo-v1.9.2 as build-backend
|
||||||
|
|
||||||
ADD backend /build/backend
|
ADD backend /build/backend
|
||||||
WORKDIR /build/backend/_example/memory_store
|
WORKDIR /build/backend/_example/memory_store
|
||||||
|
|
||||||
RUN go build -o /build/bin/memory_store -ldflags "-X main.revision=0.0.0 -s -w"
|
RUN go build -o /build/bin/memory_store -ldflags "-X main.revision=0.0.0 -s -w"
|
||||||
|
|
||||||
|
FROM umputun/baseimage:app-v1.9.2
|
||||||
|
|
||||||
FROM umputun/baseimage:app-latest
|
ARG GITHUB_SHA
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
|
||||||
|
org.opencontainers.image.description="Remark42 comment engine example JRPC memory store" \
|
||||||
|
org.opencontainers.image.documentation="https://github.com/umputun/remark42/tree/master/backend/_example/memory_store" \
|
||||||
|
org.opencontainers.image.licenses="MIT" \
|
||||||
|
org.opencontainers.image.source="https://github.com/umputun/remark42.git" \
|
||||||
|
org.opencontainers.image.title="Remark42 JRPC example memory store" \
|
||||||
|
org.opencontainers.image.url="https://remark42.com/" \
|
||||||
|
org.opencontainers.image.revision="${GITHUB_SHA}"
|
||||||
|
|
||||||
WORKDIR /srv
|
WORKDIR /srv
|
||||||
COPY --from=build-backend /build/bin/memory_store /srv/memory_store
|
COPY --from=build-backend /build/bin/memory_store /srv/memory_store
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ func NewMemAdminStore(key string) *MemAdmin {
|
|||||||
return &MemAdmin{data: map[string]AdminRec{}, key: key}
|
return &MemAdmin{data: map[string]AdminRec{}, key: key}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key executes find by siteID and returns substructure with secret key
|
// Key supposed to execute find by siteID and returns substructure with secret key,
|
||||||
|
// but in this case the shared secret is used for all sites
|
||||||
func (m *MemAdmin) Key(_ string) (key string, err error) {
|
func (m *MemAdmin) Key(_ string) (key string, err error) {
|
||||||
return m.key, nil
|
return m.key, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ package accessor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/go-pkgz/lgr"
|
||||||
"github.com/umputun/remark42/backend/app/store"
|
"github.com/umputun/remark42/backend/app/store"
|
||||||
"github.com/umputun/remark42/backend/app/store/engine"
|
"github.com/umputun/remark42/backend/app/store/engine"
|
||||||
)
|
)
|
||||||
@@ -267,7 +267,7 @@ func (m *MemData) ListFlags(req engine.FlagRequest) (res []interface{}, err erro
|
|||||||
return res, nil
|
return res, nil
|
||||||
|
|
||||||
case engine.Blocked:
|
case engine.Blocked:
|
||||||
log.Printf("%+v", m.metaUsers)
|
log.Printf("[INFO] metaUsers: %+v", m.metaUsers)
|
||||||
for _, u := range m.metaUsers {
|
for _, u := range m.metaUsers {
|
||||||
if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) {
|
if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) {
|
||||||
res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil})
|
res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil})
|
||||||
|
|||||||
@@ -624,6 +624,7 @@ func TestMemData_DeleteComment(t *testing.T) {
|
|||||||
func TestMemData_Close(t *testing.T) {
|
func TestMemData_Close(t *testing.T) {
|
||||||
b := prepMem(t)
|
b := prepMem(t)
|
||||||
assert.NoError(t, b.Close())
|
assert.NoError(t, b.Close())
|
||||||
|
assert.NoError(t, b.Close(), "second call should not result in panic or errors")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMemData_DeleteHard(t *testing.T) {
|
func TestMemData_DeleteHard(t *testing.T) {
|
||||||
|
|||||||
@@ -1,44 +1,42 @@
|
|||||||
module github.com/umputun/remark42/memory_store
|
module github.com/umputun/remark42/memory_store
|
||||||
|
|
||||||
go 1.17
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-pkgz/jrpc v0.2.0
|
github.com/go-pkgz/jrpc v0.3.0
|
||||||
github.com/go-pkgz/lgr v0.10.4
|
github.com/go-pkgz/lgr v0.11.0
|
||||||
github.com/jessevdk/go-flags v1.5.0
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/umputun/remark42/backend v1.9.0
|
github.com/umputun/remark42/backend v1.1000.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Depado/bfchroma v1.3.0 // indirect
|
github.com/Depado/bfchroma/v2 v2.0.0 // indirect
|
||||||
github.com/PuerkitoBio/goquery v1.8.0 // indirect
|
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
github.com/alecthomas/chroma/v2 v2.8.0 // indirect
|
||||||
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/didip/tollbooth/v6 v6.1.2 // indirect
|
github.com/didip/tollbooth/v7 v7.0.1 // indirect
|
||||||
github.com/didip/tollbooth_chi v0.0.0-20220429013743-da966f2f674b // indirect
|
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f // indirect
|
||||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/go-chi/chi v4.1.1+incompatible // indirect
|
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||||
github.com/go-chi/render v1.0.1 // indirect
|
github.com/go-chi/render v1.0.3 // indirect
|
||||||
github.com/go-pkgz/expirable-cache v0.0.3 // indirect
|
github.com/go-pkgz/expirable-cache v1.0.0 // indirect
|
||||||
github.com/go-pkgz/rest v1.15.0 // indirect
|
github.com/go-pkgz/rest v1.17.0 // indirect
|
||||||
github.com/gorilla/css v1.0.0 // indirect
|
github.com/gorilla/css v1.0.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.18 // indirect
|
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rs/xid v1.4.0 // indirect
|
github.com/rs/xid v1.5.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/stretchr/objx v0.3.0 // indirect
|
go.etcd.io/bbolt v1.3.7 // indirect
|
||||||
go.etcd.io/bbolt v1.3.6 // indirect
|
golang.org/x/image v0.11.0 // indirect
|
||||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect
|
golang.org/x/net v0.14.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
|
golang.org/x/sys v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/umputun/remark42/backend => ../../
|
replace github.com/umputun/remark42/backend v1.1000.0 => ../../
|
||||||
|
|||||||
@@ -1,45 +1,44 @@
|
|||||||
github.com/Depado/bfchroma v1.3.0 h1:zz14vpvySU6S0CL6yGPr1vkFevQecIt8dJdCsMS2JpM=
|
github.com/Depado/bfchroma/v2 v2.0.0 h1:IRpN9BPkNwEpR6w1ectIcNWOuhDSLx+8f1pn83fzxx8=
|
||||||
github.com/Depado/bfchroma v1.3.0/go.mod h1:c0bFk0tFmT+clD3TIGurjWCfD/QV8/EebfM3JGr+98M=
|
github.com/Depado/bfchroma/v2 v2.0.0/go.mod h1:wFwW/Pw8Tnd0irzgO9Zxtxgzp3aPS8qBWlyadxujxmw=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
|
||||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||||
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
|
||||||
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
|
||||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
|
||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/didip/tollbooth/v6 v6.0.1/go.mod h1:j2pKs+JQ5PvU/K4jFnrnwntrmfUbYLJE5oSdxR37FD0=
|
github.com/didip/tollbooth/v7 v7.0.0/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
|
||||||
github.com/didip/tollbooth/v6 v6.1.2 h1:Kdqxmqw9YTv0uKajBUiWQg+GURL/k4vy9gmLCL01PjQ=
|
github.com/didip/tollbooth/v7 v7.0.1 h1:TkT4sBKoQoHQFPf7blQ54iHrZiTDnr8TceU+MulVAog=
|
||||||
github.com/didip/tollbooth/v6 v6.1.2/go.mod h1:xjcse6CTHCLuOkzsWrEgdy9WPJFv+p/x6v+MyfP+O9s=
|
github.com/didip/tollbooth/v7 v7.0.1/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
|
||||||
github.com/didip/tollbooth_chi v0.0.0-20200524181329-8b84cd7183d9/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
|
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f h1:jtKwihcLmUC9BAhoJ9adCUqdSSZcOdH2KL7mPTUm2aw=
|
||||||
github.com/didip/tollbooth_chi v0.0.0-20220429013743-da966f2f674b h1:elkngQhLBcyDIXwL9Z7AfXXbBszUEsLxqNw6WPF8Mtc=
|
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f/go.mod h1:q9C80dnsuVRP2dAskjnXRNWdUJqtGgwG9wNrzt0019s=
|
||||||
github.com/didip/tollbooth_chi v0.0.0-20220429013743-da966f2f674b/go.mod h1:0ZVa6kSzS011nfTC1rELyxK4tjVf6vqBnOv7oY2KlsA=
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||||
github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4=
|
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
|
||||||
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
|
||||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||||
github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc=
|
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||||
github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ=
|
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||||
github.com/go-pkgz/jrpc v0.2.0 h1:CLy/eZyekjraVrxZV18N2R1mYLMJ/nWrgdfyIOGPY/E=
|
github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||||
github.com/go-pkgz/jrpc v0.2.0/go.mod h1:wd8vtQ4CgtCnuqua6x2b1SKIgv0VSOh5Dn0uUITbiUE=
|
github.com/go-pkgz/expirable-cache v1.0.0 h1:ns5+1hjY8hntGv8bPaQd9Gr7Jyo+Uw5SLyII40aQdtA=
|
||||||
github.com/go-pkgz/lgr v0.10.4 h1:l7qyFjqEZgwRgaQQSEp6tve4A3OU80VrfzpvtEX8ngw=
|
github.com/go-pkgz/expirable-cache v1.0.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||||
github.com/go-pkgz/lgr v0.10.4/go.mod h1:CD0s1z6EFpIUplV067gitF77tn25JItzwHNKAPqeCF0=
|
github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U=
|
||||||
github.com/go-pkgz/rest v1.5.0/go.mod h1:nQaM3RhSTUAmbBZWY4hfe4buyeC9VckvhoCktiQXJxI=
|
github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00=
|
||||||
github.com/go-pkgz/rest v1.15.0 h1:v/BDqJF9robo85GME85GWJ7O/NjtAO0x7LvO4EqWNRE=
|
github.com/go-pkgz/lgr v0.11.0 h1:9XH5o+vj09L0sRWEswIGK1lJ6g07xVB4/Z28RV9Z+qM=
|
||||||
github.com/go-pkgz/rest v1.15.0/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
|
github.com/go-pkgz/lgr v0.11.0/go.mod h1:4rdRmMSs4yGFjnUg0rSDbKx21LmFNZoH4y8OLl3qDnU=
|
||||||
|
github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
|
||||||
|
github.com/go-pkgz/rest v1.17.0 h1:LoBI/lDBMuqwWhOOkc6thM9NnwJO+K9nWvCOjZ7BAgE=
|
||||||
|
github.com/go-pkgz/rest v1.17.0/go.mod h1:HHlLOt02NJc2sgffXBF6hYVMcRo4Gz3vjg43zTzN7VM=
|
||||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -47,6 +46,7 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
|||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
@@ -54,65 +54,74 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.18 h1:6HcxvXDAi3ARt3slx6nTesbvorIc3QeTzBNRvWktHBo=
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||||
|
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
|
||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||||
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -43,16 +43,14 @@ func main() {
|
|||||||
adminStore := accessor.NewMemAdminStore(opts.Secret)
|
adminStore := accessor.NewMemAdminStore(opts.Secret)
|
||||||
imgStore := accessor.NewMemImageStore()
|
imgStore := accessor.NewMemImageStore()
|
||||||
|
|
||||||
rpcServer := jrpc.Server{
|
rpcServer := jrpc.NewServer(
|
||||||
API: opts.API,
|
opts.API,
|
||||||
AuthUser: opts.AuthUser,
|
jrpc.Auth(opts.AuthUser, opts.AuthPasswd),
|
||||||
AuthPasswd: opts.AuthPasswd,
|
jrpc.WithSignature("remark42-memory", "umputun", revision),
|
||||||
Version: revision,
|
jrpc.WithLogger(log.Default()),
|
||||||
AppName: "remark42-memory",
|
)
|
||||||
Logger: log.Default(),
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := server.NewRPC(dataStore, adminStore, imgStore, &rpcServer)
|
srv := server.NewRPC(dataStore, adminStore, imgStore, rpcServer)
|
||||||
|
|
||||||
admRec := accessor.AdminRec{
|
admRec := accessor.AdminRec{
|
||||||
SiteID: "remark",
|
SiteID: "remark",
|
||||||
|
|||||||
@@ -198,25 +198,62 @@ func TestRPC_listFlagsHndl(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "123456", id)
|
assert.Equal(t, "123456", id)
|
||||||
|
|
||||||
flagReq := engine.FlagRequest{
|
// verify user
|
||||||
|
verifyFlagReq := engine.FlagRequest{
|
||||||
Flag: engine.Verified,
|
Flag: engine.Verified,
|
||||||
UserID: "u1",
|
UserID: "u1",
|
||||||
Locator: store.Locator{
|
Locator: store.Locator{
|
||||||
SiteID: "test-site",
|
SiteID: "test-site",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
flags, err := re.ListFlags(flagReq)
|
flags, err := re.ListFlags(verifyFlagReq)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []interface{}{}, flags)
|
assert.Empty(t, flags)
|
||||||
|
|
||||||
flagReq.Update = engine.FlagTrue
|
verifyFlagReq.Update = engine.FlagTrue
|
||||||
status, err := re.Flag(flagReq)
|
status, err := re.Flag(verifyFlagReq)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, true, status)
|
assert.Equal(t, true, status)
|
||||||
|
|
||||||
flags, err = re.ListFlags(flagReq)
|
flags, err = re.ListFlags(verifyFlagReq)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []interface{}{"u1"}, flags)
|
assert.Equal(t, []interface{}{"u1"}, flags)
|
||||||
|
verifiedUsers := make([]string, 0, len(flags))
|
||||||
|
for _, v := range flags {
|
||||||
|
verifiedUsers = append(verifiedUsers, v.(string))
|
||||||
|
}
|
||||||
|
assert.Equal(t, []string{"u1"}, verifiedUsers)
|
||||||
|
|
||||||
|
// block user
|
||||||
|
blockFlagReq := engine.FlagRequest{
|
||||||
|
Flag: engine.Blocked,
|
||||||
|
UserID: "u1",
|
||||||
|
Locator: store.Locator{
|
||||||
|
SiteID: "test-site",
|
||||||
|
},
|
||||||
|
TTL: time.Hour,
|
||||||
|
}
|
||||||
|
flags, err = re.ListFlags(blockFlagReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, flags)
|
||||||
|
|
||||||
|
blockFlagReq.Update = engine.FlagTrue
|
||||||
|
status, err = re.Flag(blockFlagReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, true, status)
|
||||||
|
|
||||||
|
flags, err = re.ListFlags(blockFlagReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, flags)
|
||||||
|
blockedUsers := make([]store.BlockedUser, 0, len(flags))
|
||||||
|
for _, v := range flags {
|
||||||
|
blockedUsers = append(blockedUsers, v.(store.BlockedUser))
|
||||||
|
}
|
||||||
|
require.Equal(t, 1, len(blockedUsers))
|
||||||
|
blockedUserInfo := blockedUsers[0]
|
||||||
|
assert.Equal(t, "u1", blockedUserInfo.ID)
|
||||||
|
assert.True(t, blockedUserInfo.Until.After(time.Now().Add(time.Minute*59)), "blocked duration is more than 59m away")
|
||||||
|
assert.True(t, blockedUserInfo.Until.Before(time.Now().Add(time.Minute*61)), "blocked duration is less than 61m away")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRPC_userDetailHndl(t *testing.T) {
|
func TestRPC_userDetailHndl(t *testing.T) {
|
||||||
@@ -301,6 +338,6 @@ func TestRPC_closeHndl(t *testing.T) {
|
|||||||
api := fmt.Sprintf("http://localhost:%d/test", port)
|
api := fmt.Sprintf("http://localhost:%d/test", port)
|
||||||
|
|
||||||
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
|
||||||
err := re.Close()
|
assert.NoError(t, re.Close())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, re.Close(), "second call should not result in panic or errors")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func prepTestStore(t *testing.T) (port int, teardown func()) {
|
|||||||
mg := accessor.NewMemData()
|
mg := accessor.NewMemData()
|
||||||
adm := accessor.NewMemAdminStore("secret")
|
adm := accessor.NewMemAdminStore("secret")
|
||||||
img := accessor.NewMemImageStore()
|
img := accessor.NewMemImageStore()
|
||||||
s := NewRPC(mg, adm, img, &jrpc.Server{API: "/test", Logger: jrpc.NoOpLogger})
|
s := NewRPC(mg, adm, img, jrpc.NewServer("/test"))
|
||||||
|
|
||||||
admRec := accessor.AdminRec{
|
admRec := accessor.AdminRec{
|
||||||
SiteID: "test-site",
|
SiteID: "test-site",
|
||||||
|
|||||||
@@ -14,11 +14,10 @@ import (
|
|||||||
// BackupCommand set of flags and command for export
|
// BackupCommand set of flags and command for export
|
||||||
// ExportPath used as a separate element to leverage BACKUP_PATH. If ExportFile has a path (i.e. with /) BACKUP_PATH ignored.
|
// ExportPath used as a separate element to leverage BACKUP_PATH. If ExportFile has a path (i.e. with /) BACKUP_PATH ignored.
|
||||||
type BackupCommand struct {
|
type BackupCommand struct {
|
||||||
ExportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"`
|
ExportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"`
|
||||||
ExportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.TS}}.gz" description:"file name"`
|
ExportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.TS}}.gz" description:"file name"`
|
||||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
|
||||||
Timeout time.Duration `long:"timeout" default:"60m" description:"export (backup) timeout"`
|
SupportCmdOpts
|
||||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
|
||||||
CommonOpts
|
CommonOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ import (
|
|||||||
|
|
||||||
// CleanupCommand set of flags and command for cleanup
|
// CleanupCommand set of flags and command for cleanup
|
||||||
type CleanupCommand struct {
|
type CleanupCommand struct {
|
||||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
Dry bool `long:"dry" description:"dry mode, will not remove comments"`
|
||||||
Dry bool `long:"dry" description:"dry mode, will not remove comments"`
|
From string `long:"from" description:"from yyyymmdd"`
|
||||||
From string `long:"from" description:"from yyyymmdd"`
|
To string `long:"to" description:"from yyyymmdd"`
|
||||||
To string `long:"to" description:"from yyyymmdd"`
|
BadWords []string `short:"w" long:"bword" description:"bad word(s)"`
|
||||||
BadWords []string `short:"w" long:"bword" description:"bad word(s)"`
|
BadUsers []string `short:"u" long:"buser" description:"bad user(s)"`
|
||||||
BadUsers []string `short:"u" long:"buser" description:"bad user(s)"`
|
SetTitle bool `long:"title" description:"title mode, will not remove comments, but reset titles to page's title'"`
|
||||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
|
||||||
SetTitle bool `long:"title" description:"title mode, will not remove comments, but reset titles to page's title'"`
|
SupportCmdOpts
|
||||||
CommonOpts
|
CommonOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ type CommonOpts struct {
|
|||||||
Revision string
|
Revision string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SupportCmdOpts is set of commands shared among similar commands like backup/restore and such.
|
||||||
|
// Order of fields defines the help command output order.
|
||||||
|
type SupportCmdOpts struct {
|
||||||
|
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
||||||
|
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"`
|
||||||
|
Timeout time.Duration `long:"timeout" default:"60m" description:"timeout for the command run"`
|
||||||
|
}
|
||||||
|
|
||||||
// DeprecatedFlag contains information about deprecated option
|
// DeprecatedFlag contains information about deprecated option
|
||||||
type DeprecatedFlag struct {
|
type DeprecatedFlag struct {
|
||||||
Old string
|
Old string
|
||||||
|
|||||||
@@ -8,18 +8,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/go-pkgz/lgr"
|
log "github.com/go-pkgz/lgr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImportCommand set of flags and command for import
|
// ImportCommand set of flags and command for import
|
||||||
type ImportCommand struct {
|
type ImportCommand struct {
|
||||||
InputFile string `short:"f" long:"file" description:"input file name" required:"true"`
|
InputFile string `short:"f" long:"file" description:"input file name" required:"true"`
|
||||||
Provider string `short:"p" long:"provider" default:"disqus" choice:"disqus" choice:"wordpress" choice:"commento" description:"import format"` //nolint
|
Provider string `short:"p" long:"provider" default:"disqus" choice:"disqus" choice:"wordpress" choice:"commento" description:"import format"` //nolint
|
||||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
|
||||||
Timeout time.Duration `long:"timeout" default:"60m" description:"import timeout"`
|
SupportCmdOpts
|
||||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
|
||||||
CommonOpts
|
CommonOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/go-pkgz/lgr"
|
log "github.com/go-pkgz/lgr"
|
||||||
)
|
)
|
||||||
@@ -14,10 +13,9 @@ import (
|
|||||||
// RemapCommand set of flags and command for change linkage between comments to
|
// RemapCommand set of flags and command for change linkage between comments to
|
||||||
// different urls based on given rules (input file)
|
// different urls based on given rules (input file)
|
||||||
type RemapCommand struct {
|
type RemapCommand struct {
|
||||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
InputFile string `short:"f" long:"file" description:"input file name" required:"true"`
|
||||||
InputFile string `short:"f" long:"file" description:"input file name" required:"true"`
|
|
||||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
SupportCmdOpts
|
||||||
Timeout time.Duration `long:"timeout" default:"60m" description:"remap timeout"`
|
|
||||||
CommonOpts
|
CommonOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ type RestoreCommand struct {
|
|||||||
ImportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"`
|
ImportPath string `short:"p" long:"path" env:"BACKUP_PATH" default:"./var/backup" description:"export path"`
|
||||||
ImportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.YYYYMMDD}}.gz" description:"file name" required:"true"`
|
ImportFile string `short:"f" long:"file" default:"userbackup-{{.SITE}}-{{.YYYYMMDD}}.gz" description:"file name" required:"true"`
|
||||||
|
|
||||||
Site string `short:"s" long:"site" env:"SITE" default:"remark" description:"site name"`
|
SupportCmdOpts
|
||||||
Timeout time.Duration `long:"timeout" default:"60m" description:"import timeout"`
|
|
||||||
AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"`
|
|
||||||
CommonOpts
|
CommonOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,12 +27,10 @@ func (rc *RestoreCommand) Execute(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
importer := ImportCommand{
|
importer := ImportCommand{
|
||||||
InputFile: fname,
|
InputFile: fname,
|
||||||
Site: rc.Site,
|
Provider: "native",
|
||||||
Provider: "native",
|
SupportCmdOpts: rc.SupportCmdOpts,
|
||||||
Timeout: rc.Timeout,
|
CommonOpts: rc.CommonOpts,
|
||||||
AdminPasswd: rc.AdminPasswd,
|
|
||||||
CommonOpts: rc.CommonOpts,
|
|
||||||
}
|
}
|
||||||
return importer.Execute(args)
|
return importer.Execute(args)
|
||||||
}
|
}
|
||||||
|
|||||||
+105
-81
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -41,6 +42,9 @@ import (
|
|||||||
"github.com/umputun/remark42/backend/app/templates"
|
"github.com/umputun/remark42/backend/app/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed web
|
||||||
|
var webFS embed.FS
|
||||||
|
|
||||||
// ServerCommand with command line flags and env
|
// ServerCommand with command line flags and env
|
||||||
type ServerCommand struct {
|
type ServerCommand struct {
|
||||||
Store StoreGroup `group:"store" namespace:"store" env-namespace:"STORE"`
|
Store StoreGroup `group:"store" namespace:"store" env-namespace:"STORE"`
|
||||||
@@ -79,7 +83,7 @@ type ServerCommand struct {
|
|||||||
EnableEmoji bool `long:"emoji" env:"EMOJI" description:"enable emoji"`
|
EnableEmoji bool `long:"emoji" env:"EMOJI" description:"enable emoji"`
|
||||||
SimpleView bool `long:"simple-view" env:"SIMPLE_VIEW" description:"minimal comment editor mode"`
|
SimpleView bool `long:"simple-view" env:"SIMPLE_VIEW" description:"minimal comment editor mode"`
|
||||||
ProxyCORS bool `long:"proxy-cors" env:"PROXY_CORS" description:"disable internal CORS and delegate it to proxy"`
|
ProxyCORS bool `long:"proxy-cors" env:"PROXY_CORS" description:"disable internal CORS and delegate it to proxy"`
|
||||||
AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments"`
|
AllowedHosts []string `long:"allowed-hosts" env:"ALLOWED_HOSTS" description:"limit hosts/sources allowed to embed comments" env-delim:","`
|
||||||
SubscribersOnly bool `long:"subscribers-only" env:"SUBSCRIBERS_ONLY" description:"enable commenting only for Patreon subscribers"`
|
SubscribersOnly bool `long:"subscribers-only" env:"SUBSCRIBERS_ONLY" description:"enable commenting only for Patreon subscribers"`
|
||||||
DisableSignature bool `long:"disable-signature" env:"DISABLE_SIGNATURE" description:"disable server signature in headers"`
|
DisableSignature bool `long:"disable-signature" env:"DISABLE_SIGNATURE" description:"disable server signature in headers"`
|
||||||
|
|
||||||
@@ -92,16 +96,17 @@ type ServerCommand struct {
|
|||||||
SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"`
|
SendJWTHeader bool `long:"send-jwt-header" env:"SEND_JWT_HEADER" description:"send JWT as a header instead of cookie"`
|
||||||
SameSite string `long:"same-site" env:"SAME_SITE" description:"set same site policy for cookies" choice:"default" choice:"none" choice:"lax" choice:"strict" default:"default"` // nolint
|
SameSite string `long:"same-site" env:"SAME_SITE" description:"set same site policy for cookies" choice:"default" choice:"none" choice:"lax" choice:"strict" default:"default"` // nolint
|
||||||
|
|
||||||
Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"`
|
Apple AppleGroup `group:"apple" namespace:"apple" env-namespace:"APPLE" description:"Apple OAuth"`
|
||||||
Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"`
|
Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"`
|
||||||
Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"`
|
Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"`
|
||||||
Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"`
|
Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"`
|
||||||
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
|
Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"`
|
||||||
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"`
|
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
|
||||||
Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"`
|
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"`
|
||||||
Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"`
|
Patreon AuthGroup `group:"patreon" namespace:"patreon" env-namespace:"PATREON" description:"Patreon OAuth"`
|
||||||
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
|
Telegram bool `long:"telegram" env:"TELEGRAM" description:"Enable Telegram auth (using token from telegram.token)"`
|
||||||
Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"`
|
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
|
||||||
|
Anonymous bool `long:"anon" env:"ANON" description:"enable anonymous login"`
|
||||||
Email struct {
|
Email struct {
|
||||||
Enable bool `long:"enable" env:"ENABLE" description:"enable auth via email"`
|
Enable bool `long:"enable" env:"ENABLE" description:"enable auth via email"`
|
||||||
From string `long:"from" env:"FROM" description:"from email address"`
|
From string `long:"from" env:"FROM" description:"from email address"`
|
||||||
@@ -129,6 +134,14 @@ type ImageProxyGroup struct {
|
|||||||
CacheExternal bool `long:"cache-external" env:"CACHE_EXTERNAL" description:"enable caching for external images"`
|
CacheExternal bool `long:"cache-external" env:"CACHE_EXTERNAL" description:"enable caching for external images"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppleGroup defines options for Apple auth params
|
||||||
|
type AppleGroup struct {
|
||||||
|
CID string `long:"cid" env:"CID" description:"Apple client ID"`
|
||||||
|
TID string `long:"tid" env:"TID" description:"Apple service ID"`
|
||||||
|
KID string `long:"kid" env:"KID" description:"Private key ID"`
|
||||||
|
PrivateKeyFilePath string `long:"private-key-filepath" env:"PRIVATE_KEY_FILEPATH" description:"Private key file location" default:"/srv/var/apple.p8"`
|
||||||
|
}
|
||||||
|
|
||||||
// AuthGroup defines options group for auth params
|
// AuthGroup defines options group for auth params
|
||||||
type AuthGroup struct {
|
type AuthGroup struct {
|
||||||
CID string `long:"cid" env:"CID" description:"OAuth client ID"`
|
CID string `long:"cid" env:"CID" description:"OAuth client ID"`
|
||||||
@@ -193,7 +206,7 @@ type AdminGroup struct {
|
|||||||
Admins []string `long:"id" env:"ID" description:"admin(s) ids" env-delim:","`
|
Admins []string `long:"id" env:"ID" description:"admin(s) ids" env-delim:","`
|
||||||
Email []string `long:"email" env:"EMAIL" description:"admin emails" env-delim:","`
|
Email []string `long:"email" env:"EMAIL" description:"admin emails" env-delim:","`
|
||||||
} `group:"shared" namespace:"shared" env-namespace:"SHARED"`
|
} `group:"shared" namespace:"shared" env-namespace:"SHARED"`
|
||||||
RPC RPCGroup `group:"rpc" namespace:"rpc" env-namespace:"RPC"`
|
RPC AdminRPCGroup `group:"rpc" namespace:"rpc" env-namespace:"RPC"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TelegramGroup defines token for Telegram used in notify and auth modules
|
// TelegramGroup defines token for Telegram used in notify and auth modules
|
||||||
@@ -204,13 +217,14 @@ type TelegramGroup struct {
|
|||||||
|
|
||||||
// SMTPGroup defines options for SMTP server connection, used in auth and notify modules
|
// SMTPGroup defines options for SMTP server connection, used in auth and notify modules
|
||||||
type SMTPGroup struct {
|
type SMTPGroup struct {
|
||||||
Host string `long:"host" env:"HOST" description:"SMTP host"`
|
Host string `long:"host" env:"HOST" description:"SMTP host"`
|
||||||
Port int `long:"port" env:"PORT" description:"SMTP port"`
|
Port int `long:"port" env:"PORT" description:"SMTP port"`
|
||||||
Username string `long:"username" env:"USERNAME" description:"SMTP user name"`
|
Username string `long:"username" env:"USERNAME" description:"SMTP user name"`
|
||||||
Password string `long:"password" env:"PASSWORD" description:"SMTP password"`
|
Password string `long:"password" env:"PASSWORD" description:"SMTP password"`
|
||||||
TLS bool `long:"tls" env:"TLS" description:"enable TLS"`
|
TLS bool `long:"tls" env:"TLS" description:"enable TLS"`
|
||||||
StartTLS bool `long:"starttls" env:"STARTTLS" description:"enable StartTLS"`
|
LoginAuth bool `long:"login_auth" env:"LOGIN_AUTH" description:"enable LOGIN auth instead of PLAIN"`
|
||||||
TimeOut time.Duration `long:"timeout" env:"TIMEOUT" default:"10s" description:"SMTP TCP connection timeout"`
|
StartTLS bool `long:"starttls" env:"STARTTLS" description:"enable StartTLS"`
|
||||||
|
TimeOut time.Duration `long:"timeout" env:"TIMEOUT" default:"10s" description:"SMTP TCP connection timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyGroup defines options for notification
|
// NotifyGroup defines options for notification
|
||||||
@@ -260,6 +274,12 @@ type RPCGroup struct {
|
|||||||
AuthPassword string `long:"auth_passwd" env:"AUTH_PASSWD" description:"basic auth user password"`
|
AuthPassword string `long:"auth_passwd" env:"AUTH_PASSWD" description:"basic auth user password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminRPCGroup defines options for remote admin store
|
||||||
|
type AdminRPCGroup struct {
|
||||||
|
RPCGroup
|
||||||
|
SecretPerSite bool `long:"secret_per_site" env:"SECRET_PER_SITE" description:"enable JWT secret retrieval per aud, which is site_id in this case"`
|
||||||
|
}
|
||||||
|
|
||||||
// LoadingCache defines interface for caching
|
// LoadingCache defines interface for caching
|
||||||
type LoadingCache interface {
|
type LoadingCache interface {
|
||||||
Get(key cache.Key, fn func() ([]byte, error)) (data []byte, err error) // load from cache if found or put to cache and return
|
Get(key cache.Key, fn func() ([]byte, error)) (data []byte, err error) // load from cache if found or put to cache and return
|
||||||
@@ -505,11 +525,12 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
|||||||
authenticator := s.getAuthenticator(dataService, avatarStore, adminStore, authRefreshCache)
|
authenticator := s.getAuthenticator(dataService, avatarStore, adminStore, authRefreshCache)
|
||||||
|
|
||||||
telegramAuth := s.makeTelegramAuth(authenticator) // telegram auth requires TelegramAPI listener which is constructed below
|
telegramAuth := s.makeTelegramAuth(authenticator) // telegram auth requires TelegramAPI listener which is constructed below
|
||||||
telegramService, telegramBotUsername := s.startTelegramAuthAndNotify(ctx, telegramAuth)
|
telegramService := s.startTelegramAuthAndNotify(ctx, telegramAuth)
|
||||||
|
|
||||||
err = s.addAuthProviders(authenticator)
|
err = s.addAuthProviders(authenticator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = dataService.Close()
|
_ = dataService.Close()
|
||||||
|
_ = authRefreshCache.Close()
|
||||||
return nil, fmt.Errorf("failed to make authenticator: %w", err)
|
return nil, fmt.Errorf("failed to make authenticator: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,36 +570,38 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
|||||||
sslConfig, err := s.makeSSLConfig()
|
sslConfig, err := s.makeSSLConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = dataService.Close()
|
_ = dataService.Close()
|
||||||
|
_ = authRefreshCache.Close()
|
||||||
return nil, fmt.Errorf("failed to make config of ssl server params: %w", err)
|
return nil, fmt.Errorf("failed to make config of ssl server params: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := &api.Rest{
|
srv := &api.Rest{
|
||||||
Version: s.Revision,
|
Version: s.Revision,
|
||||||
DataService: dataService,
|
DataService: dataService,
|
||||||
WebRoot: s.WebRoot,
|
WebRoot: s.WebRoot,
|
||||||
RemarkURL: s.RemarkURL,
|
WebFS: webFS,
|
||||||
ImageProxy: imgProxy,
|
RemarkURL: s.RemarkURL,
|
||||||
CommentFormatter: commentFormatter,
|
ImageProxy: imgProxy,
|
||||||
Migrator: migr,
|
CommentFormatter: commentFormatter,
|
||||||
ReadOnlyAge: s.ReadOnlyAge,
|
Migrator: migr,
|
||||||
SharedSecret: s.SharedSecret,
|
ReadOnlyAge: s.ReadOnlyAge,
|
||||||
Authenticator: authenticator,
|
SharedSecret: s.SharedSecret,
|
||||||
Cache: loadingCache,
|
Authenticator: authenticator,
|
||||||
NotifyService: notifyService,
|
Cache: loadingCache,
|
||||||
TelegramService: telegramService,
|
NotifyService: notifyService,
|
||||||
SSLConfig: sslConfig,
|
TelegramService: telegramService,
|
||||||
UpdateLimiter: s.UpdateLimit,
|
SSLConfig: sslConfig,
|
||||||
ImageService: imageService,
|
UpdateLimiter: s.UpdateLimit,
|
||||||
EmailNotifications: contains("email", s.Notify.Users),
|
ImageService: imageService,
|
||||||
TelegramBotUsername: telegramBotUsername,
|
EmailNotifications: contains("email", s.Notify.Users),
|
||||||
EmojiEnabled: s.EnableEmoji,
|
TelegramNotifications: contains("telegram", s.Notify.Users) && telegramService != nil,
|
||||||
AnonVote: s.AnonymousVote && s.RestrictVoteIP,
|
EmojiEnabled: s.EnableEmoji,
|
||||||
SimpleView: s.SimpleView,
|
AnonVote: s.AnonymousVote && s.RestrictVoteIP,
|
||||||
ProxyCORS: s.ProxyCORS,
|
SimpleView: s.SimpleView,
|
||||||
AllowedAncestors: s.AllowedHosts,
|
ProxyCORS: s.ProxyCORS,
|
||||||
SendJWTHeader: s.Auth.SendJWTHeader,
|
AllowedAncestors: s.AllowedHosts,
|
||||||
SubscribersOnly: s.SubscribersOnly,
|
SendJWTHeader: s.Auth.SendJWTHeader,
|
||||||
DisableSignature: s.DisableSignature,
|
SubscribersOnly: s.SubscribersOnly,
|
||||||
|
DisableSignature: s.DisableSignature,
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
||||||
@@ -588,6 +611,7 @@ func (s *ServerCommand) newServerApp(ctx context.Context) (*serverApp, error) {
|
|||||||
da, errDevAuth := authenticator.DevAuth()
|
da, errDevAuth := authenticator.DevAuth()
|
||||||
if errDevAuth != nil {
|
if errDevAuth != nil {
|
||||||
_ = dataService.Close()
|
_ = dataService.Close()
|
||||||
|
_ = authRefreshCache.Close()
|
||||||
return nil, fmt.Errorf("can't make dev oauth2 server: %w", errDevAuth)
|
return nil, fmt.Errorf("can't make dev oauth2 server: %w", errDevAuth)
|
||||||
}
|
}
|
||||||
devAuth = da
|
devAuth = da
|
||||||
@@ -823,12 +847,28 @@ func (s *ServerCommand) makeCache() (LoadingCache, error) {
|
|||||||
return nil, fmt.Errorf("unsupported cache type %s", s.Cache.Type)
|
return nil, fmt.Errorf("unsupported cache type %s", s.Cache.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo // simple code but many if checks
|
||||||
func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
||||||
providersCount := 0
|
providersCount := 0
|
||||||
if s.Auth.Telegram {
|
if s.Auth.Telegram {
|
||||||
providersCount++
|
providersCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Auth.Apple.CID != "" && s.Auth.Apple.TID != "" && s.Auth.Apple.KID != "" {
|
||||||
|
err := authenticator.AddAppleProvider(
|
||||||
|
provider.AppleConfig{
|
||||||
|
ClientID: s.Auth.Apple.CID,
|
||||||
|
TeamID: s.Auth.Apple.TID,
|
||||||
|
KeyID: s.Auth.Apple.KID,
|
||||||
|
ResponseMode: "query", // default is form_post which wouldn't work here
|
||||||
|
},
|
||||||
|
provider.LoadApplePrivateKeyFromFile(s.Auth.Apple.PrivateKeyFilePath),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
providersCount++
|
||||||
|
}
|
||||||
if s.Auth.Google.CID != "" && s.Auth.Google.CSEC != "" {
|
if s.Auth.Google.CID != "" && s.Auth.Google.CSEC != "" {
|
||||||
authenticator.AddProvider("google", s.Auth.Google.CID, s.Auth.Google.CSEC)
|
authenticator.AddProvider("google", s.Auth.Google.CID, s.Auth.Google.CSEC)
|
||||||
providersCount++
|
providersCount++
|
||||||
@@ -860,7 +900,11 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
|||||||
|
|
||||||
if s.Auth.Dev {
|
if s.Auth.Dev {
|
||||||
log.Print("[INFO] dev access enabled")
|
log.Print("[INFO] dev access enabled")
|
||||||
authenticator.AddProvider("dev", "", "")
|
u, errURL := url.Parse(s.RemarkURL)
|
||||||
|
if errURL != nil {
|
||||||
|
return fmt.Errorf("can't parse Remark42 URL: %w", errURL)
|
||||||
|
}
|
||||||
|
authenticator.AddDevProvider(u.Hostname(), 8084)
|
||||||
providersCount++
|
providersCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,17 +915,20 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
|||||||
SMTPUserName: s.SMTP.Username,
|
SMTPUserName: s.SMTP.Username,
|
||||||
SMTPPassword: s.SMTP.Password,
|
SMTPPassword: s.SMTP.Password,
|
||||||
TimeOut: s.SMTP.TimeOut,
|
TimeOut: s.SMTP.TimeOut,
|
||||||
|
StartTLS: s.SMTP.StartTLS,
|
||||||
|
LoginAuth: s.SMTP.LoginAuth,
|
||||||
TLS: s.SMTP.TLS,
|
TLS: s.SMTP.TLS,
|
||||||
|
Charset: "UTF-8",
|
||||||
From: s.Auth.Email.From,
|
From: s.Auth.Email.From,
|
||||||
Subject: s.Auth.Email.Subject,
|
Subject: s.Auth.Email.Subject,
|
||||||
ContentType: s.Auth.Email.ContentType,
|
ContentType: s.Auth.Email.ContentType,
|
||||||
}
|
}
|
||||||
sndr := sender.NewEmailClient(params, log.Default())
|
sndr := sender.NewEmailClient(params, log.Default())
|
||||||
tmpl, err := s.loadEmailTemplate()
|
tmpl, err := templates.Read(s.Auth.Email.MsgTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
authenticator.AddVerifProvider("email", tmpl, sndr)
|
authenticator.AddVerifProvider("email", string(tmpl), sndr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Auth.Anonymous {
|
if s.Auth.Anonymous {
|
||||||
@@ -925,27 +972,6 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadEmailTemplate trying to get template from statik
|
|
||||||
func (s *ServerCommand) loadEmailTemplate() (string, error) {
|
|
||||||
var file []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if s.Auth.Email.MsgTemplate == "email_confirmation_login.html.tmpl" {
|
|
||||||
fs := templates.NewFS()
|
|
||||||
file, err = fs.ReadFile(s.Auth.Email.MsgTemplate)
|
|
||||||
} else {
|
|
||||||
// deprecated loading from an external file, should be removed before v1.9.0
|
|
||||||
file, err = os.ReadFile(s.Auth.Email.MsgTemplate)
|
|
||||||
log.Printf("[INFO] template %s will be read from disk", s.Auth.Email.MsgTemplate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to read file %s: %w", s.Auth.Email.MsgTemplate, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(file), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates and registers telegram auth, which we need separately from other auth providers
|
// creates and registers telegram auth, which we need separately from other auth providers
|
||||||
func (s *ServerCommand) makeTelegramAuth(authenticator *auth.Service) providers.TGUpdatesReceiver {
|
func (s *ServerCommand) makeTelegramAuth(authenticator *auth.Service) providers.TGUpdatesReceiver {
|
||||||
if s.Auth.Telegram {
|
if s.Auth.Telegram {
|
||||||
@@ -967,7 +993,8 @@ func (s *ServerCommand) makeNotifyService(dataStore *service.DataStore, destinat
|
|||||||
if destinations == nil {
|
if destinations == nil {
|
||||||
destinations = []notify.Destination{}
|
destinations = []notify.Destination{}
|
||||||
}
|
}
|
||||||
if telegram != nil {
|
// it's possible that telegram notification service was created for auth but should not be used for notifications
|
||||||
|
if telegram != nil && (contains("telegram", s.Notify.Users) || contains("telegram", s.Notify.Admins)) {
|
||||||
destinations = append(destinations, telegram)
|
destinations = append(destinations, telegram)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1042,6 +1069,7 @@ func (s *ServerCommand) makeNotifyDestinations(authenticator *auth.Service) ([]n
|
|||||||
Port: s.SMTP.Port,
|
Port: s.SMTP.Port,
|
||||||
TLS: s.SMTP.TLS,
|
TLS: s.SMTP.TLS,
|
||||||
StartTLS: s.SMTP.StartTLS,
|
StartTLS: s.SMTP.StartTLS,
|
||||||
|
LoginAuth: s.SMTP.LoginAuth,
|
||||||
Username: s.SMTP.Username,
|
Username: s.SMTP.Username,
|
||||||
Password: s.SMTP.Password,
|
Password: s.SMTP.Password,
|
||||||
TimeOut: s.SMTP.TimeOut,
|
TimeOut: s.SMTP.TimeOut,
|
||||||
@@ -1118,7 +1146,7 @@ func (s *ServerCommand) getAuthenticator(ds *service.DataStore, avas avatar.Stor
|
|||||||
SameSiteCookie: s.parseSameSite(s.Auth.SameSite),
|
SameSiteCookie: s.parseSameSite(s.Auth.SameSite),
|
||||||
SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"),
|
SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"),
|
||||||
SecretReader: token.SecretFunc(func(aud string) (string, error) { // get secret per site
|
SecretReader: token.SecretFunc(func(aud string) (string, error) { // get secret per site
|
||||||
return admns.Key("")
|
return admns.Key(aud)
|
||||||
}),
|
}),
|
||||||
ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { // set attributes, on new token or refresh
|
ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { // set attributes, on new token or refresh
|
||||||
if c.User == nil {
|
if c.User == nil {
|
||||||
@@ -1163,6 +1191,7 @@ func (s *ServerCommand) getAuthenticator(ds *service.DataStore, avas avatar.Stor
|
|||||||
Logger: log.Default(),
|
Logger: log.Default(),
|
||||||
RefreshCache: authRefreshCache,
|
RefreshCache: authRefreshCache,
|
||||||
UseGravatar: true,
|
UseGravatar: true,
|
||||||
|
AudSecrets: s.Admin.RPC.SecretPerSite,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1183,20 +1212,15 @@ func (s *ServerCommand) parseSameSite(ss string) http.SameSite {
|
|||||||
|
|
||||||
// startTelegramAuthAndNotify initializes telegram notify and auth Telegram Bot listen loop.
|
// startTelegramAuthAndNotify initializes telegram notify and auth Telegram Bot listen loop.
|
||||||
// Does nothing if telegram auth and notifications are disabled.
|
// Does nothing if telegram auth and notifications are disabled.
|
||||||
// Doesn't return telegram bot username if user notifications are disabled, as that is the way frontend knows they are enabled.
|
func (s *ServerCommand) startTelegramAuthAndNotify(ctx context.Context, telegramAuth providers.TGUpdatesReceiver) (tg *notify.Telegram) {
|
||||||
func (s *ServerCommand) startTelegramAuthAndNotify(ctx context.Context, telegramAuth providers.TGUpdatesReceiver) (tg *notify.Telegram, telegramBotUsername string) {
|
|
||||||
if !contains("telegram", s.Notify.Users) && !contains("telegram", s.Notify.Admins) && !s.Auth.Telegram {
|
if !contains("telegram", s.Notify.Users) && !contains("telegram", s.Notify.Admins) && !s.Auth.Telegram {
|
||||||
return nil, ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if tg, err = s.makeTelegramNotify(); err != nil {
|
if tg, err = s.makeTelegramNotify(); err != nil {
|
||||||
log.Printf("[WARN] failed to make telegram notify service, %s", err)
|
log.Printf("[WARN] failed to make telegram notify service, %s", err)
|
||||||
return nil, ""
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
if contains("telegram", s.Notify.Users) {
|
|
||||||
telegramBotUsername = tg.GetBotUsername()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
telegramReceivers := []providers.TGUpdatesReceiver{tg}
|
telegramReceivers := []providers.TGUpdatesReceiver{tg}
|
||||||
@@ -1206,7 +1230,7 @@ func (s *ServerCommand) startTelegramAuthAndNotify(ctx context.Context, telegram
|
|||||||
// start bot messages receiver for both notify and auth services
|
// start bot messages receiver for both notify and auth services
|
||||||
go providers.DispatchTelegramUpdates(ctx, tg, telegramReceivers, time.Second*5)
|
go providers.DispatchTelegramUpdates(ctx, tg, telegramReceivers, time.Second*5)
|
||||||
|
|
||||||
return tg, telegramBotUsername
|
return tg
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitAtCommas split s at commas, ignoring commas in strings.
|
// splitAtCommas split s at commas, ignoring commas in strings.
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func TestServerApp_DevMode(t *testing.T) {
|
|||||||
waitForHTTPServerStart(port)
|
waitForHTTPServerStart(port)
|
||||||
|
|
||||||
providers := app.restSrv.Authenticator.Providers()
|
providers := app.restSrv.Authenticator.Providers()
|
||||||
require.Equal(t, 9+1, len(providers), "extra auth provider")
|
require.Equal(t, 10+1, len(providers), "extra auth provider")
|
||||||
assert.Equal(t, "dev", providers[len(providers)-2].Name(), "dev auth provider")
|
assert.Equal(t, "dev", providers[len(providers)-2].Name(), "dev auth provider")
|
||||||
// send ping
|
// send ping
|
||||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/v1/ping", port))
|
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/v1/ping", port))
|
||||||
@@ -107,7 +107,7 @@ func TestServerApp_AnonMode(t *testing.T) {
|
|||||||
waitForHTTPServerStart(port)
|
waitForHTTPServerStart(port)
|
||||||
|
|
||||||
providers := app.restSrv.Authenticator.Providers()
|
providers := app.restSrv.Authenticator.Providers()
|
||||||
require.Equal(t, 9+1, len(providers), "extra auth provider for anon")
|
require.Equal(t, 10+1, len(providers), "extra auth provider for anon")
|
||||||
assert.Equal(t, "anonymous", providers[len(providers)-1].Name(), "anon auth provider")
|
assert.Equal(t, "anonymous", providers[len(providers)-1].Name(), "anon auth provider")
|
||||||
|
|
||||||
client := http.Client{Timeout: 10 * time.Second}
|
client := http.Client{Timeout: 10 * time.Second}
|
||||||
@@ -290,7 +290,8 @@ func TestServerApp_WithRemote(t *testing.T) {
|
|||||||
port := chooseRandomUnusedPort()
|
port := chooseRandomUnusedPort()
|
||||||
_, err := p.ParseArgs([]string{"--admin-passwd=password", "--cache.type=none",
|
_, err := p.ParseArgs([]string{"--admin-passwd=password", "--cache.type=none",
|
||||||
"--store.type=rpc", "--store.rpc.api=http://127.0.0.1",
|
"--store.type=rpc", "--store.rpc.api=http://127.0.0.1",
|
||||||
"--port=" + strconv.Itoa(port), "--admin.type=rpc", "--admin.rpc.api=http://127.0.0.1", "--avatar.fs.path=/tmp"})
|
"--port=" + strconv.Itoa(port), "--avatar.fs.path=/tmp",
|
||||||
|
"--admin.type=rpc", "--admin.rpc.secret_per_site", "--admin.rpc.api=http://127.0.0.1"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid"
|
opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid"
|
||||||
opts.BackupLocation, opts.Image.FS.Path = "/tmp", "/tmp"
|
opts.BackupLocation, opts.Image.FS.Path = "/tmp", "/tmp"
|
||||||
@@ -351,6 +352,7 @@ func TestServerApp_Failed(t *testing.T) {
|
|||||||
assert.EqualError(t, err, "invalid remark42 url demo.remark42.com")
|
assert.EqualError(t, err, "invalid remark42 url demo.remark42.com")
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
|
|
||||||
|
// wrong store type
|
||||||
opts = ServerCommand{}
|
opts = ServerCommand{}
|
||||||
opts.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "123456"})
|
opts.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "123456"})
|
||||||
|
|
||||||
@@ -374,6 +376,19 @@ func TestServerApp_Failed(t *testing.T) {
|
|||||||
"problem subscribing to channel remark42-cache on address wrong_address: "+
|
"problem subscribing to channel remark42-cache on address wrong_address: "+
|
||||||
"dial tcp: address wrong_address: missing port in address")
|
"dial tcp: address wrong_address: missing port in address")
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
|
|
||||||
|
// wrong apple private key type
|
||||||
|
opts = ServerCommand{}
|
||||||
|
opts.SetCommon(CommonOpts{RemarkURL: "https://demo.remark42.com", SharedSecret: "123456"})
|
||||||
|
p = flags.NewParser(&opts, flags.Default)
|
||||||
|
_, err = p.ParseArgs([]string{"--auth.apple.cid=123", "--auth.apple.tid=123",
|
||||||
|
"--auth.apple.kid=123", "--auth.apple.private-key-filepath=testdata/apple-bad.p8"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = opts.newServerApp(context.Background())
|
||||||
|
assert.EqualError(t, err,
|
||||||
|
"failed to make authenticator: an AppleProvider creating failed: "+
|
||||||
|
"provided private key is not ECDSA")
|
||||||
|
t.Log(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerApp_Shutdown(t *testing.T) {
|
func TestServerApp_Shutdown(t *testing.T) {
|
||||||
@@ -667,19 +682,6 @@ func TestServerAuthHooks(t *testing.T) {
|
|||||||
client.CloseIdleConnections()
|
client.CloseIdleConnections()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_loadEmailTemplate(t *testing.T) {
|
|
||||||
cmd := ServerCommand{}
|
|
||||||
cmd.Auth.Email.MsgTemplate = "testdata/email.tmpl"
|
|
||||||
r, err := cmd.loadEmailTemplate()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "The token is {{.Token}}", r)
|
|
||||||
|
|
||||||
cmd.Auth.Email.MsgTemplate = "badpath.tmpl"
|
|
||||||
r, err = cmd.loadEmailTemplate()
|
|
||||||
assert.EqualError(t, err, "failed to read file badpath.tmpl: open badpath.tmpl: no such file or directory")
|
|
||||||
assert.Equal(t, r, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerCommand_parseSameSite(t *testing.T) {
|
func TestServerCommand_parseSameSite(t *testing.T) {
|
||||||
tbl := []struct {
|
tbl := []struct {
|
||||||
inp string
|
inp string
|
||||||
@@ -771,6 +773,8 @@ func prepServerApp(t *testing.T, fn func(o ServerCommand) ServerCommand) (*serve
|
|||||||
cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation, cmd.Image.FS.Path = "/tmp/remark42_test", "fs", "/tmp/remark42_test", "/tmp/remark42_test"
|
cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation, cmd.Image.FS.Path = "/tmp/remark42_test", "fs", "/tmp/remark42_test", "/tmp/remark42_test"
|
||||||
cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port)
|
cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port)
|
||||||
cmd.Store.Bolt.Timeout = 10 * time.Second
|
cmd.Store.Bolt.Timeout = 10 * time.Second
|
||||||
|
cmd.Auth.Apple.CID, cmd.Auth.Apple.KID, cmd.Auth.Apple.TID = "cid", "kid", "tid"
|
||||||
|
cmd.Auth.Apple.PrivateKeyFilePath = "testdata/apple.p8"
|
||||||
cmd.Auth.Github.CSEC, cmd.Auth.Github.CID = "csec", "cid"
|
cmd.Auth.Github.CSEC, cmd.Auth.Github.CID = "csec", "cid"
|
||||||
cmd.Auth.Google.CSEC, cmd.Auth.Google.CID = "csec", "cid"
|
cmd.Auth.Google.CSEC, cmd.Auth.Google.CID = "csec", "cid"
|
||||||
cmd.Auth.Facebook.CSEC, cmd.Auth.Facebook.CID = "csec", "cid"
|
cmd.Auth.Facebook.CSEC, cmd.Auth.Facebook.CID = "csec", "cid"
|
||||||
@@ -817,8 +821,6 @@ func createAppFromCmd(t *testing.T, cmd ServerCommand) (*serverApp, context.Cont
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
app, err := cmd.newServerApp(ctx)
|
app, err := cmd.newServerApp(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
return app, ctx, cancel
|
return app, ctx, cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKNwapOQ6rQJHetP
|
||||||
|
HRlJBIh1OsOsUBiXb3rXXE3xpWAxAha0MH+UPRblOko+5T2JqIb+xKf9Vi3oTM3t
|
||||||
|
KvffaOPtzKXZauscjq6NGzA3LgeiMy6q19pvkUUOlGYK6+Xfl+B7Xw6+hBMkQuGE
|
||||||
|
nUS8nkpR5mK4ne7djIyfHFfMu4ptAgMBAAECgYA+s0PPtMq1osG9oi4xoxeAGikf
|
||||||
|
JB3eMUptP+2DYW7mRibc+ueYKhB9lhcUoKhlQUhL8bUUFVZYakP8xD21thmQqnC4
|
||||||
|
f63asad0ycteJMLb3r+z26LHuCyOdPg1pyLk3oQ32lVQHBCYathRMcVznxOG16VK
|
||||||
|
I8BFfstJTaJu0lK/wQJBANYFGusBiZsJQ3utrQMVPpKmloO2++4q1v6ZR4puDQHx
|
||||||
|
TjLjAIgrkYfwTJBLBRZxec0E7TmuVQ9uJ+wMu/+7zaUCQQDDf2xMnQqYknJoKGq+
|
||||||
|
oAnyC66UqWC5xAnQS32mlnJ632JXA0pf9pb1SXAYExB1p9Dfqd3VAwQDwBsDDgP6
|
||||||
|
HD8pAkEA0lscNQZC2TaGtKZk2hXkdcH1SKru/g3vWTkRHxfCAznJUaza1fx0wzdG
|
||||||
|
GcES1Bdez0tbW4llI5By/skZc2eE3QJAFl6fOskBbGHde3Oce0F+wdZ6XIJhEgCP
|
||||||
|
iukIcKZoZQzoiMJUoVRrA5gqnmaYDI5uRRl/y57zt6YksR3KcLUIuQJAd242M/WF
|
||||||
|
6YAZat3q/wEeETeQq1wrooew+8lHl05/Nt0cCpV48RGEhJ83pzBm3mnwHf8lTBJH
|
||||||
|
x6XroMXsmbnsEw==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgGH2MylyZjjRdauTk
|
||||||
|
xxXW6p8VSHqIeVRRKSJPg1xn6+KgCgYIKoZIzj0DAQehRANCAAS/mNzQ7aBbIBr3
|
||||||
|
DiHiJGIDEzi6+q3mmyhH6ZWQWFdFei2qgdyM1V6qtRPVq+yHBNSBebbR4noE/IYO
|
||||||
|
hMdWYrKn
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
This stub page would be replaced by the frontend statically built HTML during the Docker image build.
|
||||||
+3
-3
@@ -23,7 +23,8 @@ type Opts struct {
|
|||||||
CleanupCmd cmd.CleanupCommand `command:"cleanup"`
|
CleanupCmd cmd.CleanupCommand `command:"cleanup"`
|
||||||
RemapCmd cmd.RemapCommand `command:"remap"`
|
RemapCmd cmd.RemapCommand `command:"remap"`
|
||||||
|
|
||||||
RemarkURL string `long:"url" env:"REMARK_URL" required:"true" description:"url to remark"`
|
RemarkURL string `long:"url" env:"REMARK_URL" required:"true" description:"url to remark"`
|
||||||
|
// SharedSecret is only used in server command, but defined for all commands for historical reasons
|
||||||
SharedSecret string `long:"secret" env:"SECRET" required:"true" description:"the shared secret key used to sign JWT, should be a random, long, hard-to-guess string"`
|
SharedSecret string `long:"secret" env:"SECRET" required:"true" description:"the shared secret key used to sign JWT, should be a random, long, hard-to-guess string"`
|
||||||
|
|
||||||
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
|
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
|
||||||
@@ -56,9 +57,8 @@ func main() {
|
|||||||
if _, err := p.Parse(); err != nil {
|
if _, err := p.Parse(); err != nil {
|
||||||
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
|
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
} else {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -22,7 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Test_Main(t *testing.T) {
|
func Test_Main(t *testing.T) {
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "remark42")
|
dir, err := os.MkdirTemp(os.TempDir(), "remark42")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ func Test_Main(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMain_WithWebhook(t *testing.T) {
|
func TestMain_WithWebhook(t *testing.T) {
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "remark42")
|
dir, err := os.MkdirTemp(os.TempDir(), "remark42")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (ab AutoBackup) removeOldBackupFiles() {
|
|||||||
backFiles = append(backFiles, info)
|
backFiles = append(backFiles, info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Slice(backFiles, func(i int, j int) bool { return backFiles[i].Name() < backFiles[j].Name() })
|
sort.Slice(backFiles, func(i, j int) bool { return backFiles[i].Name() < backFiles[j].Name() })
|
||||||
|
|
||||||
if len(backFiles) > ab.KeepMax {
|
if len(backFiles) > ab.KeepMax {
|
||||||
for i := 0; i < len(backFiles)-ab.KeepMax; i++ {
|
for i := 0; i < len(backFiles)-ab.KeepMax; i++ {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ type MapperMaker func(reader io.Reader) (Mapper, error)
|
|||||||
type Store interface {
|
type Store interface {
|
||||||
Create(comment store.Comment) (commentID string, err error)
|
Create(comment store.Comment) (commentID string, err error)
|
||||||
Find(locator store.Locator, sort string, user store.User) ([]store.Comment, error)
|
Find(locator store.Locator, sort string, user store.User) ([]store.Comment, error)
|
||||||
List(siteID string, limit int, skip int) ([]store.PostInfo, error)
|
List(siteID string, limit, skip int) ([]store.PostInfo, error)
|
||||||
DeleteAll(siteID string) error
|
DeleteAll(siteID string) error
|
||||||
Metas(siteID string) (umetas []service.UserMetaData, pmetas []service.PostMetaData, err error)
|
Metas(siteID string) (umetas []service.UserMetaData, pmetas []service.PostMetaData, err error)
|
||||||
SetMetas(siteID string, umetas []service.UserMetaData, pmetas []service.PostMetaData) error
|
SetMetas(siteID string, umetas []service.UserMetaData, pmetas []service.PostMetaData) error
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ func TestNative_ImportManyWithError(t *testing.T) {
|
|||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
buf.WriteString(`{"version":1, "users":[], "posts":[]}` + "\n")
|
buf.WriteString(`{"version":1, "users":[], "posts":[]}` + "\n")
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
buf.WriteString(fmt.Sprintf(goodRec, i))
|
fmt.Fprintf(buf, goodRec, i)
|
||||||
}
|
}
|
||||||
buf.WriteString("{}\n")
|
buf.WriteString("{}\n")
|
||||||
buf.WriteString("{}\n")
|
buf.WriteString("{}\n")
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ func NewEmail(emailParams EmailParams, smtpParams ntf.SMTPParams) (*Email, error
|
|||||||
func (e *Email) setTemplates() error {
|
func (e *Email) setTemplates() error {
|
||||||
var err error
|
var err error
|
||||||
var msgTmplFile, verifyTmplFile []byte
|
var msgTmplFile, verifyTmplFile []byte
|
||||||
fs := templates.NewFS()
|
|
||||||
|
|
||||||
if e.VerificationTemplatePath == "" {
|
if e.VerificationTemplatePath == "" {
|
||||||
e.VerificationTemplatePath = defaultEmailVerificationTemplatePath
|
e.VerificationTemplatePath = defaultEmailVerificationTemplatePath
|
||||||
@@ -110,10 +109,10 @@ func (e *Email) setTemplates() error {
|
|||||||
e.MsgTemplatePath = defaultEmailTemplatePath
|
e.MsgTemplatePath = defaultEmailTemplatePath
|
||||||
}
|
}
|
||||||
|
|
||||||
if msgTmplFile, err = fs.ReadFile(e.MsgTemplatePath); err != nil {
|
if msgTmplFile, err = templates.Read(e.MsgTemplatePath); err != nil {
|
||||||
return fmt.Errorf("can't read message template: %w", err)
|
return fmt.Errorf("can't read message template: %w", err)
|
||||||
}
|
}
|
||||||
if verifyTmplFile, err = fs.ReadFile(e.VerificationTemplatePath); err != nil {
|
if verifyTmplFile, err = templates.Read(e.VerificationTemplatePath); err != nil {
|
||||||
return fmt.Errorf("can't read verification template: %w", err)
|
return fmt.Errorf("can't read verification template: %w", err)
|
||||||
}
|
}
|
||||||
if e.msgTmpl, err = template.New("msgTmpl").Parse(string(msgTmplFile)); err != nil {
|
if e.msgTmpl, err = template.New("msgTmpl").Parse(string(msgTmplFile)); err != nil {
|
||||||
|
|||||||
@@ -55,32 +55,18 @@ func Test_initTemplatesErr(t *testing.T) {
|
|||||||
errText string
|
errText string
|
||||||
emailParams EmailParams
|
emailParams EmailParams
|
||||||
}{
|
}{
|
||||||
{
|
|
||||||
name: "with wrong (default, working in prod) path to reply template",
|
|
||||||
errText: "can't read message template: open email_reply.html.tmpl: no such file or directory",
|
|
||||||
emailParams: EmailParams{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with wrong (default, working in prod) path to verification template",
|
|
||||||
errText: "can't read verification template: open email_confirmation_subscription.html.tmpl: no such file or directory",
|
|
||||||
emailParams: EmailParams{
|
|
||||||
MsgTemplatePath: "testdata/msg.html.tmpl",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "with wrong path to verification template",
|
name: "with wrong path to verification template",
|
||||||
errText: "can't read verification template: open notfound.tmpl: no such file or directory",
|
errText: "notfound.tmpl: file does not exist",
|
||||||
emailParams: EmailParams{
|
emailParams: EmailParams{
|
||||||
VerificationTemplatePath: "notfound.tmpl",
|
VerificationTemplatePath: "notfound.tmpl",
|
||||||
MsgTemplatePath: "testdata/msg.html.tmpl",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with wrong path to message template",
|
name: "with wrong path to message template",
|
||||||
errText: "can't read message template: open notfound.tmpl: no such file or directory",
|
errText: "notfound.tmpl: file does not exist",
|
||||||
emailParams: EmailParams{
|
emailParams: EmailParams{
|
||||||
VerificationTemplatePath: "testdata/verification.html.tmpl",
|
MsgTemplatePath: "notfound.tmpl",
|
||||||
MsgTemplatePath: "notfound.tmpl",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ type Destination interface {
|
|||||||
// Store defines the minimal interface accessing stored comments used by notifier
|
// Store defines the minimal interface accessing stored comments used by notifier
|
||||||
type Store interface {
|
type Store interface {
|
||||||
Get(locator store.Locator, id string, user store.User) (store.Comment, error)
|
Get(locator store.Locator, id string, user store.User) (store.Comment, error)
|
||||||
GetUserEmail(siteID string, userID string) (string, error)
|
GetUserEmail(siteID, userID string) (string, error)
|
||||||
GetUserTelegram(siteID string, userID string) (string, error)
|
GetUserTelegram(siteID, userID string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// used for email and telegram retrieval from user details
|
// used for email and telegram retrieval from user details
|
||||||
@@ -143,6 +143,12 @@ func (s *Service) SubmitVerification(req VerificationRequest) {
|
|||||||
// Close queue channel and wait for completion
|
// Close queue channel and wait for completion
|
||||||
func (s *Service) Close() {
|
func (s *Service) Close() {
|
||||||
if s.queue != nil {
|
if s.queue != nil {
|
||||||
|
// don't panic in case service is already closed
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
log.Print("[DEBUG] close notifier")
|
log.Print("[DEBUG] close notifier")
|
||||||
close(s.queue)
|
close(s.queue)
|
||||||
close(s.verificationQueue)
|
close(s.verificationQueue)
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ func TestService_NoDestinations(t *testing.T) {
|
|||||||
s.Submit(Request{Comment: store.Comment{ID: "123"}})
|
s.Submit(Request{Comment: store.Comment{ID: "123"}})
|
||||||
s.Submit(Request{Comment: store.Comment{ID: "123"}})
|
s.Submit(Request{Comment: store.Comment{ID: "123"}})
|
||||||
s.Close()
|
s.Close()
|
||||||
|
// second call should not result in panic
|
||||||
|
s.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_WithDestinations(t *testing.T) {
|
func TestService_WithDestinations(t *testing.T) {
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ type admin struct {
|
|||||||
|
|
||||||
type adminStore interface {
|
type adminStore interface {
|
||||||
Delete(locator store.Locator, commentID string, mode store.DeleteMode) error
|
Delete(locator store.Locator, commentID string, mode store.DeleteMode) error
|
||||||
DeleteUser(siteID string, userID string, mode store.DeleteMode) error
|
DeleteUser(siteID, userID string, mode store.DeleteMode) error
|
||||||
DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error
|
DeleteUserDetail(siteID, userID string, detail engine.UserDetail) error
|
||||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||||
IsBlocked(siteID string, userID string) bool
|
IsBlocked(siteID, userID string) bool
|
||||||
SetBlock(siteID string, userID string, status bool, ttl time.Duration) error
|
SetBlock(siteID, userID string, status bool, ttl time.Duration) error
|
||||||
BlockedUsers(siteID string) ([]store.BlockedUser, error)
|
BlockedUsers(siteID string) ([]store.BlockedUser, error)
|
||||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
||||||
SetTitle(locator store.Locator, commentID string) (comment store.Comment, err error)
|
SetTitle(locator store.Locator, commentID string) (comment store.Comment, err error)
|
||||||
SetVerified(siteID string, userID string, status bool) error
|
SetVerified(siteID, userID string, status bool) error
|
||||||
SetReadOnly(locator store.Locator, status bool) error
|
SetReadOnly(locator store.Locator, status bool) error
|
||||||
SetPin(locator store.Locator, commentID string, status bool) error
|
SetPin(locator store.Locator, commentID string, status bool) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -164,7 +163,7 @@ func (m *Migrator) remapCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "remap failed, bad given rules", rest.ErrDecode)
|
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "remap failed, bad given rules", rest.ErrDecode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer r.Body.Close()
|
defer r.Body.Close() //nolint gosec // we don't care about response body
|
||||||
|
|
||||||
// start remap procedure with mapper
|
// start remap procedure with mapper
|
||||||
go func() {
|
go func() {
|
||||||
@@ -172,7 +171,7 @@ func (m *Migrator) remapCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer m.setBusy(siteID, false)
|
defer m.setBusy(siteID, false)
|
||||||
|
|
||||||
// do export
|
// do export
|
||||||
fh, e := ioutil.TempFile("", "remark42_convert")
|
fh, e := os.CreateTemp("", "remark42_convert")
|
||||||
if e != nil {
|
if e != nil {
|
||||||
log.Printf("[WARN] failed to make temp file %+v", e)
|
log.Printf("[WARN] failed to make temp file %+v", e)
|
||||||
return
|
return
|
||||||
@@ -250,7 +249,7 @@ func (m *Migrator) runImport(siteID, provider, tmpfile string) {
|
|||||||
|
|
||||||
// saveTemp reads from reader and saves to temp file
|
// saveTemp reads from reader and saves to temp file
|
||||||
func (m *Migrator) saveTemp(r io.Reader) (string, error) {
|
func (m *Migrator) saveTemp(r io.Reader) (string, error) {
|
||||||
tmpfile, err := ioutil.TempFile("", "remark42_import")
|
tmpfile, err := os.CreateTemp("", "remark42_import")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("can't make temp file: %w", err)
|
return "", fmt.Errorf("can't make temp file: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ package api
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/didip/tollbooth/v6"
|
"github.com/didip/tollbooth/v7"
|
||||||
"github.com/didip/tollbooth_chi"
|
"github.com/didip/tollbooth_chi"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
@@ -23,7 +26,6 @@ import (
|
|||||||
log "github.com/go-pkgz/lgr"
|
log "github.com/go-pkgz/lgr"
|
||||||
R "github.com/go-pkgz/rest"
|
R "github.com/go-pkgz/rest"
|
||||||
"github.com/go-pkgz/rest/logger"
|
"github.com/go-pkgz/rest/logger"
|
||||||
"github.com/rakyll/statik/fs"
|
|
||||||
|
|
||||||
"github.com/umputun/remark42/backend/app/notify"
|
"github.com/umputun/remark42/backend/app/notify"
|
||||||
"github.com/umputun/remark42/backend/app/rest"
|
"github.com/umputun/remark42/backend/app/rest"
|
||||||
@@ -31,7 +33,6 @@ import (
|
|||||||
"github.com/umputun/remark42/backend/app/store"
|
"github.com/umputun/remark42/backend/app/store"
|
||||||
"github.com/umputun/remark42/backend/app/store/image"
|
"github.com/umputun/remark42/backend/app/store/image"
|
||||||
"github.com/umputun/remark42/backend/app/store/service"
|
"github.com/umputun/remark42/backend/app/store/service"
|
||||||
"github.com/umputun/remark42/backend/app/templates"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rest is a rest access server
|
// Rest is a rest access server
|
||||||
@@ -50,6 +51,7 @@ type Rest struct {
|
|||||||
|
|
||||||
AnonVote bool
|
AnonVote bool
|
||||||
WebRoot string
|
WebRoot string
|
||||||
|
WebFS embed.FS
|
||||||
RemarkURL string
|
RemarkURL string
|
||||||
ReadOnlyAge int
|
ReadOnlyAge int
|
||||||
SharedSecret string
|
SharedSecret string
|
||||||
@@ -57,16 +59,16 @@ type Rest struct {
|
|||||||
Low int
|
Low int
|
||||||
Critical int
|
Critical int
|
||||||
}
|
}
|
||||||
UpdateLimiter float64
|
UpdateLimiter float64
|
||||||
EmailNotifications bool
|
EmailNotifications bool
|
||||||
TelegramBotUsername string
|
TelegramNotifications bool
|
||||||
EmojiEnabled bool
|
EmojiEnabled bool
|
||||||
SimpleView bool
|
SimpleView bool
|
||||||
ProxyCORS bool
|
ProxyCORS bool
|
||||||
SendJWTHeader bool
|
SendJWTHeader bool
|
||||||
AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..."
|
AllowedAncestors []string // sets Content-Security-Policy "frame-ancestors ..."
|
||||||
SubscribersOnly bool
|
SubscribersOnly bool
|
||||||
DisableSignature bool // prevent signature from being added to headers
|
DisableSignature bool // prevent signature from being added to headers
|
||||||
|
|
||||||
SSLConfig SSLConfig
|
SSLConfig SSLConfig
|
||||||
httpsServer *http.Server
|
httpsServer *http.Server
|
||||||
@@ -226,7 +228,7 @@ func (s *Rest) routes() chi.Router {
|
|||||||
router.Group(func(r chi.Router) {
|
router.Group(func(r chi.Router) {
|
||||||
r.Use(middleware.Timeout(5 * time.Second))
|
r.Use(middleware.Timeout(5 * time.Second))
|
||||||
r.Use(logInfoWithBody, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(2, nil)), middleware.NoCache)
|
r.Use(logInfoWithBody, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(2, nil)), middleware.NoCache)
|
||||||
r.Use(validEmaiAuth()) // reject suspicious email logins
|
r.Use(validEmailAuth()) // reject suspicious email logins
|
||||||
r.Mount("/auth", authHandler)
|
r.Mount("/auth", authHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -347,14 +349,13 @@ func (s *Rest) routes() chi.Router {
|
|||||||
router.Group(func(rroot chi.Router) {
|
router.Group(func(rroot chi.Router) {
|
||||||
rroot.Use(middleware.Timeout(10 * time.Second))
|
rroot.Use(middleware.Timeout(10 * time.Second))
|
||||||
rroot.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(50, nil)))
|
rroot.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(50, nil)))
|
||||||
rroot.Get("/index.html", s.pubRest.getStartedCtrl)
|
|
||||||
rroot.Get("/robots.txt", s.pubRest.robotsCtrl)
|
rroot.Get("/robots.txt", s.pubRest.robotsCtrl)
|
||||||
rroot.Get("/email/unsubscribe.html", s.privRest.emailUnsubscribeCtrl)
|
rroot.Get("/email/unsubscribe.html", s.privRest.emailUnsubscribeCtrl)
|
||||||
rroot.Post("/email/unsubscribe.html", s.privRest.emailUnsubscribeCtrl)
|
rroot.Post("/email/unsubscribe.html", s.privRest.emailUnsubscribeCtrl)
|
||||||
})
|
})
|
||||||
|
|
||||||
// file server for static content from /web
|
// file server for static content from s.WebRoot on path /web
|
||||||
addFileServer(router, "/web", http.Dir(s.WebRoot), s.Version)
|
addFileServer(router, s.WebFS, s.WebRoot, s.Version)
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +366,6 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) {
|
|||||||
imageService: s.ImageService,
|
imageService: s.ImageService,
|
||||||
commentFormatter: s.CommentFormatter,
|
commentFormatter: s.CommentFormatter,
|
||||||
readOnlyAge: s.ReadOnlyAge,
|
readOnlyAge: s.ReadOnlyAge,
|
||||||
webRoot: s.WebRoot,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
privGrp := private{
|
privGrp := private{
|
||||||
@@ -379,7 +379,6 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) {
|
|||||||
telegramService: s.TelegramService,
|
telegramService: s.TelegramService,
|
||||||
remarkURL: s.RemarkURL,
|
remarkURL: s.RemarkURL,
|
||||||
anonVote: s.AnonVote,
|
anonVote: s.AnonVote,
|
||||||
templates: templates.NewFS(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
admGrp := admin{
|
admGrp := admin{
|
||||||
@@ -415,44 +414,44 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
emails, _ := s.DataService.AdminStore.Email(siteID)
|
emails, _ := s.DataService.AdminStore.Email(siteID)
|
||||||
|
|
||||||
cnf := struct {
|
cnf := struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
EditDuration int `json:"edit_duration"`
|
EditDuration int `json:"edit_duration"`
|
||||||
AdminEdit bool `json:"admin_edit"`
|
AdminEdit bool `json:"admin_edit"`
|
||||||
MaxCommentSize int `json:"max_comment_size"`
|
MaxCommentSize int `json:"max_comment_size"`
|
||||||
Admins []string `json:"admins"`
|
Admins []string `json:"admins"`
|
||||||
AdminEmail string `json:"admin_email"`
|
AdminEmail string `json:"admin_email"`
|
||||||
Auth []string `json:"auth_providers"`
|
Auth []string `json:"auth_providers"`
|
||||||
AnonVote bool `json:"anon_vote"`
|
AnonVote bool `json:"anon_vote"`
|
||||||
LowScore int `json:"low_score"`
|
LowScore int `json:"low_score"`
|
||||||
CriticalScore int `json:"critical_score"`
|
CriticalScore int `json:"critical_score"`
|
||||||
PositiveScore bool `json:"positive_score"`
|
PositiveScore bool `json:"positive_score"`
|
||||||
ReadOnlyAge int `json:"readonly_age"`
|
ReadOnlyAge int `json:"readonly_age"`
|
||||||
MaxImageSize int `json:"max_image_size"`
|
MaxImageSize int `json:"max_image_size"`
|
||||||
EmailNotifications bool `json:"email_notifications"`
|
EmailNotifications bool `json:"email_notifications"`
|
||||||
TelegramBotUsername string `json:"telegram_bot_username"`
|
TelegramNotifications bool `json:"telegram_notifications"`
|
||||||
EmojiEnabled bool `json:"emoji_enabled"`
|
EmojiEnabled bool `json:"emoji_enabled"`
|
||||||
SimpleView bool `json:"simple_view"`
|
SimpleView bool `json:"simple_view"`
|
||||||
SendJWTHeader bool `json:"send_jwt_header"`
|
SendJWTHeader bool `json:"send_jwt_header"`
|
||||||
SubscribersOnly bool `json:"subscribers_only"`
|
SubscribersOnly bool `json:"subscribers_only"`
|
||||||
}{
|
}{
|
||||||
Version: s.Version,
|
Version: s.Version,
|
||||||
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
||||||
AdminEdit: s.DataService.AdminEdits,
|
AdminEdit: s.DataService.AdminEdits,
|
||||||
MaxCommentSize: s.DataService.MaxCommentSize,
|
MaxCommentSize: s.DataService.MaxCommentSize,
|
||||||
Admins: admins,
|
Admins: admins,
|
||||||
AdminEmail: emails,
|
AdminEmail: emails,
|
||||||
LowScore: s.ScoreThresholds.Low,
|
LowScore: s.ScoreThresholds.Low,
|
||||||
CriticalScore: s.ScoreThresholds.Critical,
|
CriticalScore: s.ScoreThresholds.Critical,
|
||||||
PositiveScore: s.DataService.PositiveScore,
|
PositiveScore: s.DataService.PositiveScore,
|
||||||
ReadOnlyAge: s.ReadOnlyAge,
|
ReadOnlyAge: s.ReadOnlyAge,
|
||||||
MaxImageSize: s.ImageService.MaxSize,
|
MaxImageSize: s.ImageService.MaxSize,
|
||||||
EmailNotifications: s.EmailNotifications,
|
EmailNotifications: s.EmailNotifications,
|
||||||
TelegramBotUsername: s.TelegramBotUsername,
|
TelegramNotifications: s.TelegramNotifications,
|
||||||
EmojiEnabled: s.EmojiEnabled,
|
EmojiEnabled: s.EmojiEnabled,
|
||||||
AnonVote: s.AnonVote,
|
AnonVote: s.AnonVote,
|
||||||
SimpleView: s.SimpleView,
|
SimpleView: s.SimpleView,
|
||||||
SendJWTHeader: s.SendJWTHeader,
|
SendJWTHeader: s.SendJWTHeader,
|
||||||
SubscribersOnly: s.SubscribersOnly,
|
SubscribersOnly: s.SubscribersOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
cnf.Auth = []string{}
|
cnf.Auth = []string{}
|
||||||
@@ -467,34 +466,28 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
render.JSON(w, r, cnf)
|
render.JSON(w, r, cnf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serves static files from /web or embedded by statik
|
// serves static files from the webRoot directory or files embedded into the compiled binary if that directory is absent
|
||||||
func addFileServer(r chi.Router, path string, root http.FileSystem, version string) {
|
func addFileServer(r chi.Router, embedFS embed.FS, webRoot, version string) {
|
||||||
var webFS http.Handler
|
var webFS http.Handler
|
||||||
|
|
||||||
statikFS, err := fs.New()
|
if _, err := os.Stat(webRoot); err == nil {
|
||||||
if err != nil {
|
log.Printf("[INFO] run file server from %s from the disk", webRoot)
|
||||||
log.Printf("[DEBUG] no embedded assets loaded, %s", err)
|
webFS = http.FileServer(http.Dir(webRoot))
|
||||||
log.Printf("[INFO] run file server for %s, path %s", root, path)
|
|
||||||
webFS = http.FileServer(root)
|
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[INFO] run file server for %s, embedded", root)
|
log.Printf("[INFO] run file server, embedded")
|
||||||
webFS = http.FileServer(statikFS)
|
var contentFS, _ = fs.Sub(embedFS, "web")
|
||||||
|
webFS = http.FileServer(http.FS(contentFS))
|
||||||
}
|
}
|
||||||
|
|
||||||
origPath := path
|
webFS = http.StripPrefix("/web", webFS)
|
||||||
webFS = http.StripPrefix(path, webFS)
|
r.Get("/web", http.RedirectHandler("/web/", http.StatusMovedPermanently).ServeHTTP)
|
||||||
if path != "/" && path[len(path)-1] != '/' {
|
|
||||||
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
|
|
||||||
path += "/"
|
|
||||||
}
|
|
||||||
path += "*"
|
|
||||||
|
|
||||||
r.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(20, nil)),
|
r.With(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(20, nil)),
|
||||||
middleware.Timeout(10*time.Second),
|
middleware.Timeout(10*time.Second),
|
||||||
cacheControl(time.Hour, version),
|
cacheControl(time.Hour, version),
|
||||||
).Get(path, func(w http.ResponseWriter, r *http.Request) {
|
).Get("/web/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// don't show dirs, just serve files
|
// don't show dirs, just serve files
|
||||||
if strings.HasSuffix(r.URL.Path, "/") && len(r.URL.Path) > 1 && r.URL.Path != (origPath+"/") {
|
if strings.HasSuffix(r.URL.Path, "/") && len(r.URL.Path) > 1 && r.URL.Path != ("/web/") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -653,12 +646,12 @@ func subscribersOnly(enable bool) func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validEmaiAuth is a middleware for auth endpoints for email method.
|
// validEmailAuth is a middleware for auth endpoints for email method.
|
||||||
// it rejects login request if user, site or email are suspicious
|
// it rejects login request if user, site or email are suspicious
|
||||||
func validEmaiAuth() func(http.Handler) http.Handler {
|
func validEmailAuth() func(http.Handler) http.Handler {
|
||||||
|
|
||||||
reUser := regexp.MustCompile(`^[\p{L}\d\s_]{4,64}$`) // matches ui side validation, adding min/max limitation
|
reUser := regexp.MustCompile(`^[\p{L}\d\s_]{4,64}$`) // matches ui side validation, adding min/max limitation
|
||||||
reSite := regexp.MustCompile(`^[a-zA-Z\d\s_]{1,64}$`)
|
reSite := regexp.MustCompile(`^[a-zA-Z\d\s_.-]{1,64}$`)
|
||||||
|
|
||||||
return func(h http.Handler) http.Handler {
|
return func(h http.Handler) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -671,6 +664,7 @@ func validEmaiAuth() func(http.Handler) http.Handler {
|
|||||||
|
|
||||||
if u := r.URL.Query().Get("user"); u != "" {
|
if u := r.URL.Query().Get("user"); u != "" {
|
||||||
if !reUser.MatchString(u) {
|
if !reUser.MatchString(u) {
|
||||||
|
log.Printf("[WARN] suspicious user rejected: %s", u)
|
||||||
http.Error(w, "Access denied", http.StatusForbidden)
|
http.Error(w, "Access denied", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -678,6 +672,7 @@ func validEmaiAuth() func(http.Handler) http.Handler {
|
|||||||
|
|
||||||
if a := r.URL.Query().Get("address"); a != "" {
|
if a := r.URL.Query().Get("address"); a != "" {
|
||||||
if _, err := mail.ParseAddress(a); err != nil {
|
if _, err := mail.ParseAddress(a); err != nil {
|
||||||
|
log.Printf("[WARN] suspicious address rejected: %s", a)
|
||||||
http.Error(w, "Access denied", http.StatusForbidden)
|
http.Error(w, "Access denied", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -685,6 +680,7 @@ func validEmaiAuth() func(http.Handler) http.Handler {
|
|||||||
|
|
||||||
if s := r.URL.Query().Get("site"); s != "" {
|
if s := r.URL.Query().Get("site"); s != "" {
|
||||||
if !reSite.MatchString(s) {
|
if !reSite.MatchString(s) {
|
||||||
|
log.Printf("[WARN] suspicious site rejected: %s", s)
|
||||||
http.Error(w, "Access denied", http.StatusForbidden)
|
http.Error(w, "Access denied", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ type private struct {
|
|||||||
telegramService telegramService
|
telegramService telegramService
|
||||||
remarkURL string
|
remarkURL string
|
||||||
anonVote bool
|
anonVote bool
|
||||||
templates templates.FileReader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// telegramService is a subset of Telegram service used for setting up user telegram notifications
|
// telegramService is a subset of Telegram service used for setting up user telegram notifications
|
||||||
@@ -59,15 +58,15 @@ type privStore interface {
|
|||||||
Vote(req service.VoteReq) (comment store.Comment, err error)
|
Vote(req service.VoteReq) (comment store.Comment, err error)
|
||||||
Get(locator store.Locator, commentID string, user store.User) (store.Comment, error)
|
Get(locator store.Locator, commentID string, user store.User) (store.Comment, error)
|
||||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||||
GetUserEmail(siteID string, userID string) (string, error)
|
GetUserEmail(siteID, userID string) (string, error)
|
||||||
SetUserEmail(siteID string, userID string, value string) (string, error)
|
SetUserEmail(siteID, userID, value string) (string, error)
|
||||||
GetUserTelegram(siteID string, userID string) (string, error)
|
GetUserTelegram(siteID, userID string) (string, error)
|
||||||
SetUserTelegram(siteID string, userID string, value string) (string, error)
|
SetUserTelegram(siteID, userID, value string) (string, error)
|
||||||
DeleteUserDetail(siteID string, userID string, detail engine.UserDetail) error
|
DeleteUserDetail(siteID, userID string, detail engine.UserDetail) error
|
||||||
ValidateComment(c *store.Comment) error
|
ValidateComment(c *store.Comment) error
|
||||||
IsVerified(siteID string, userID string) bool
|
IsVerified(siteID, userID string) bool
|
||||||
IsReadOnly(locator store.Locator) bool
|
IsReadOnly(locator store.Locator) bool
|
||||||
IsBlocked(siteID string, userID string) bool
|
IsBlocked(siteID, userID string) bool
|
||||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,11 +90,11 @@ func (s *private) previewCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
comment = s.commentFormatter.Format(comment)
|
comment = s.commentFormatter.Format(comment)
|
||||||
comment.Sanitize()
|
comment.Sanitize()
|
||||||
|
|
||||||
// check if images are valid
|
// check if images are valid, omit proxied images as they are lazy-loaded
|
||||||
for _, id := range s.imageService.ExtractPictures(comment.Text) {
|
for _, id := range s.imageService.ExtractNonProxiedPictures(comment.Text) {
|
||||||
err := s.imageService.ResetCleanupTimer(id)
|
err := s.imageService.ResetCleanupTimer(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't renew staged picture cleanup timer", rest.ErrImgNotFound)
|
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't load picture from the comment", rest.ErrImgNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,8 +129,8 @@ func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
comment = s.commentFormatter.Format(comment)
|
comment = s.commentFormatter.Format(comment)
|
||||||
|
|
||||||
// check if images are valid
|
// check if images are valid, omit proxied images as they are lazy-loaded
|
||||||
for _, id := range s.imageService.ExtractPictures(comment.Text) {
|
for _, id := range s.imageService.ExtractNonProxiedPictures(comment.Text) {
|
||||||
_, err := s.imageService.Load(id)
|
_, err := s.imageService.Load(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't load picture from the comment", rest.ErrImgNotFound)
|
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't load picture from the comment", rest.ErrImgNotFound)
|
||||||
@@ -188,7 +187,7 @@ func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
}{}
|
}{}
|
||||||
|
|
||||||
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil {
|
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &edit); err != nil {
|
||||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment", rest.ErrDecode)
|
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't read comment details from body", rest.ErrDecode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,28 +307,59 @@ func (s *private) getEmailCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sendEmailConfirmationCtrl gets address and siteID from query, makes confirmation token and sends it to user.
|
// sendEmailConfirmationCtrl gets address and siteID from query, makes confirmation token and sends it to user.
|
||||||
// GET /email/subscribe?site=siteID&address=someone@example.com
|
// In case user is logged in with the same email, and auto_confirm is true, confirm it right away.
|
||||||
|
// In case of quick confirmation, "updated" is set to true, otherwise - to false.
|
||||||
|
// POST /email/subscribe with site and address in json body
|
||||||
|
//
|
||||||
//nolint:dupl // too hard to deduplicate that logic, as then it's tricky to use SendErrorJSON
|
//nolint:dupl // too hard to deduplicate that logic, as then it's tricky to use SendErrorJSON
|
||||||
func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Request) {
|
func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Request) {
|
||||||
user := rest.MustGetUserInfo(r)
|
user := rest.MustGetUserInfo(r)
|
||||||
address := r.URL.Query().Get("address")
|
|
||||||
siteID := r.URL.Query().Get("site")
|
subscribe := struct {
|
||||||
if address == "" {
|
Site string
|
||||||
|
Address string
|
||||||
|
autoConfirm bool
|
||||||
|
}{autoConfirm: true}
|
||||||
|
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &subscribe); err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// old behavior fallback, reading from the query params. Auto confirm is false in this case.
|
||||||
|
subscribe.Address = r.URL.Query().Get("address")
|
||||||
|
subscribe.Site = r.URL.Query().Get("site")
|
||||||
|
subscribe.autoConfirm = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if subscribe.Address == "" {
|
||||||
rest.SendErrorJSON(w, r, http.StatusBadRequest,
|
rest.SendErrorJSON(w, r, http.StatusBadRequest,
|
||||||
fmt.Errorf("missing parameter"), "address parameter is required", rest.ErrInternal)
|
fmt.Errorf("missing parameter"), "address parameter is required", rest.ErrInternal)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
existingAddress, err := s.dataService.GetUserEmail(siteID, user.ID)
|
existingAddress, getErr := s.dataService.GetUserEmail(subscribe.Site, user.ID)
|
||||||
if err != nil {
|
if getErr != nil {
|
||||||
log.Printf("[WARN] can't read email for %s, %v", user.ID, err)
|
log.Printf("[WARN] can't read email for %s, %v", user.ID, getErr)
|
||||||
}
|
}
|
||||||
if address == existingAddress {
|
if subscribe.Address == existingAddress {
|
||||||
rest.SendErrorJSON(w, r, http.StatusConflict,
|
rest.SendErrorJSON(w, r, http.StatusConflict,
|
||||||
fmt.Errorf("already verified"), "email address is already verified for this user", rest.ErrInternal)
|
fmt.Errorf("already verified"), "email address is already verified for this user", rest.ErrInternal)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// in case the user logged in with the same email as they try to subscribe with, confirm it right away
|
||||||
|
// this behavior is different from the previous one and is hidden behind the autoConfirm flag,
|
||||||
|
// which is true for the new API, and false for the old one
|
||||||
|
//
|
||||||
|
// nolint:gosec // this is not used for security purposes
|
||||||
|
if subscribe.autoConfirm &&
|
||||||
|
strings.HasPrefix(user.ID, "email_") &&
|
||||||
|
strings.TrimPrefix(user.ID, "email_") == token.HashID(sha1.New(), subscribe.Address) {
|
||||||
|
s.setEmail(w, r, user.ID, subscribe.Site, subscribe.Address)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
claims := token.Claims{
|
claims := token.Claims{
|
||||||
Handshake: &token.Handshake{ID: user.ID + "::" + address},
|
Handshake: &token.Handshake{ID: user.ID + "::" + subscribe.Address},
|
||||||
StandardClaims: jwt.StandardClaims{
|
StandardClaims: jwt.StandardClaims{
|
||||||
Audience: r.URL.Query().Get("site"),
|
Audience: r.URL.Query().Get("site"),
|
||||||
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
|
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
|
||||||
@@ -346,14 +376,14 @@ func (s *private) sendEmailConfirmationCtrl(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
s.notifyService.SubmitVerification(
|
s.notifyService.SubmitVerification(
|
||||||
notify.VerificationRequest{
|
notify.VerificationRequest{
|
||||||
SiteID: siteID,
|
SiteID: subscribe.Site,
|
||||||
User: user.Name,
|
User: user.Name,
|
||||||
Email: address,
|
Email: subscribe.Address,
|
||||||
Token: tkn,
|
Token: tkn,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
render.JSON(w, r, R.JSON{"user": user, "address": address})
|
render.JSON(w, r, R.JSON{"user": user, "address": subscribe.Address, "updated": false})
|
||||||
}
|
}
|
||||||
|
|
||||||
// telegramSubscribeCtrl generates and verifies telegram notification request
|
// telegramSubscribeCtrl generates and verifies telegram notification request
|
||||||
@@ -400,7 +430,7 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request)
|
|||||||
var address, siteID string
|
var address, siteID string
|
||||||
address, siteID, err := s.telegramService.CheckToken(queryToken, user.ID)
|
address, siteID, err := s.telegramService.CheckToken(queryToken, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't set telegram for user", rest.ErrInternal)
|
rest.SendErrorJSON(w, r, http.StatusNotFound, err, "request is not verified yet", rest.ErrInternal)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,17 +447,29 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setConfirmedEmailCtrl uses provided token parameter (generated by sendEmailConfirmationCtrl) to set email and add it to user token
|
// setConfirmedEmailCtrl uses provided token parameter (generated by sendEmailConfirmationCtrl) to set email and add it to user token
|
||||||
// PUT /email/confirm?site=siteID&tkn=jwt
|
// POST /email/confirm with site and token in json body
|
||||||
func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request) {
|
func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request) {
|
||||||
tkn := r.URL.Query().Get("tkn")
|
user := rest.MustGetUserInfo(r)
|
||||||
if tkn == "" {
|
|
||||||
|
confirm := struct {
|
||||||
|
Site string
|
||||||
|
Token string
|
||||||
|
}{}
|
||||||
|
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &confirm); err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't parse request body", rest.ErrDecode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// old behavior fallback, reading from the query params
|
||||||
|
confirm.Token = r.URL.Query().Get("tkn")
|
||||||
|
confirm.Site = r.URL.Query().Get("site")
|
||||||
|
}
|
||||||
|
|
||||||
|
if confirm.Token == "" {
|
||||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal)
|
rest.SendErrorJSON(w, r, http.StatusBadRequest, fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := rest.MustGetUserInfo(r)
|
confClaims, err := s.authenticator.TokenService().Parse(confirm.Token)
|
||||||
siteID := r.URL.Query().Get("site")
|
|
||||||
|
|
||||||
confClaims, err := s.authenticator.TokenService().Parse(tkn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
|
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
|
||||||
return
|
return
|
||||||
@@ -445,17 +487,20 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
address := elems[1]
|
address := elems[1]
|
||||||
|
s.setEmail(w, r, user.ID, confirm.Site, address)
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("[DEBUG] set email for user %s", user.ID)
|
func (s *private) setEmail(w http.ResponseWriter, r *http.Request, userID, siteID, address string) {
|
||||||
|
log.Printf("[DEBUG] set email for user %s", userID)
|
||||||
|
|
||||||
val, err := s.dataService.SetUserEmail(siteID, user.ID, address)
|
val, err := s.dataService.SetUserEmail(siteID, userID, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
code := parseError(err, rest.ErrInternal)
|
code := parseError(err, rest.ErrInternal)
|
||||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set email for user", code)
|
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't set email for user", code)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// update User.Email from the token
|
// update User.Email field
|
||||||
claims, _, err := s.authenticator.TokenService().Get(r)
|
claims, _, err := s.authenticator.TokenService().Get(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
|
rest.SendErrorJSON(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
|
||||||
@@ -473,29 +518,26 @@ func (s *private) setConfirmedEmailCtrl(w http.ResponseWriter, r *http.Request)
|
|||||||
func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||||
tkn := r.URL.Query().Get("tkn")
|
tkn := r.URL.Query().Get("tkn")
|
||||||
if tkn == "" {
|
if tkn == "" {
|
||||||
rest.SendErrorHTML(w, r, http.StatusBadRequest,
|
rest.SendErrorHTML(w, r, http.StatusBadRequest, fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal)
|
||||||
fmt.Errorf("missing parameter"), "token parameter is required", rest.ErrInternal, s.templates)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
siteID := r.URL.Query().Get("site")
|
siteID := r.URL.Query().Get("site")
|
||||||
|
|
||||||
confClaims, err := s.authenticator.TokenService().Parse(tkn)
|
confClaims, err := s.authenticator.TokenService().Parse(tkn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rest.SendErrorHTML(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal, s.templates)
|
rest.SendErrorHTML(w, r, http.StatusForbidden, err, "failed to verify confirmation token", rest.ErrInternal)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.authenticator.TokenService().IsExpired(confClaims) {
|
if s.authenticator.TokenService().IsExpired(confClaims) {
|
||||||
rest.SendErrorHTML(w, r, http.StatusForbidden,
|
rest.SendErrorHTML(w, r, http.StatusForbidden, fmt.Errorf("expired"), "failed to verify confirmation token", rest.ErrInternal)
|
||||||
fmt.Errorf("expired"), "failed to verify confirmation token", rest.ErrInternal, s.templates)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handshake.ID is user.ID + "::" + address
|
// Handshake.ID is user.ID + "::" + address
|
||||||
elems := strings.Split(confClaims.Handshake.ID, "::")
|
elems := strings.Split(confClaims.Handshake.ID, "::")
|
||||||
if len(elems) != 2 {
|
if len(elems) != 2 {
|
||||||
rest.SendErrorHTML(w, r, http.StatusBadRequest,
|
rest.SendErrorHTML(w, r, http.StatusBadRequest, fmt.Errorf("%s", confClaims.Handshake.ID), "invalid handshake token", rest.ErrInternal)
|
||||||
fmt.Errorf("%s", confClaims.Handshake.ID), "invalid handshake token", rest.ErrInternal, s.templates)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userID := elems[0]
|
userID := elems[0]
|
||||||
@@ -508,14 +550,11 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Printf("[WARN] can't read email for %s, %v", userID, err)
|
log.Printf("[WARN] can't read email for %s, %v", userID, err)
|
||||||
}
|
}
|
||||||
if existingAddress == "" {
|
if existingAddress == "" {
|
||||||
rest.SendErrorHTML(w, r, http.StatusConflict,
|
rest.SendErrorHTML(w, r, http.StatusConflict, fmt.Errorf("user is not subscribed"), "user does not have active email subscription", rest.ErrInternal)
|
||||||
fmt.Errorf("user is not subscribed"), "user does not have active email subscription", rest.ErrInternal, s.templates)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if address != existingAddress {
|
if address != existingAddress {
|
||||||
rest.SendErrorHTML(w, r, http.StatusBadRequest,
|
rest.SendErrorHTML(w, r, http.StatusBadRequest, fmt.Errorf("wrong email unsubscription"), "email address in request does not match known for this user", rest.ErrInternal)
|
||||||
fmt.Errorf("wrong email unsubscription"), "email address in request does not match known for this user",
|
|
||||||
rest.ErrInternal, s.templates)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,7 +562,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err = s.dataService.DeleteUserDetail(siteID, userID, engine.UserEmail); err != nil {
|
if err = s.dataService.DeleteUserDetail(siteID, userID, engine.UserEmail); err != nil {
|
||||||
code := parseError(err, rest.ErrInternal)
|
code := parseError(err, rest.ErrInternal)
|
||||||
rest.SendErrorHTML(w, r, http.StatusBadRequest, err, "can't delete email for user", code, s.templates)
|
rest.SendErrorHTML(w, r, http.StatusBadRequest, err, "can't delete email for user", code)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// clean User.Email from the token, if user has the token
|
// clean User.Email from the token, if user has the token
|
||||||
@@ -534,7 +573,7 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
if claims.User != nil && claims.User.Email != "" {
|
if claims.User != nil && claims.User.Email != "" {
|
||||||
claims.User.Email = ""
|
claims.User.Email = ""
|
||||||
if _, err = s.authenticator.TokenService().Set(w, claims); err != nil {
|
if _, err = s.authenticator.TokenService().Set(w, claims); err != nil {
|
||||||
rest.SendErrorHTML(w, r, http.StatusInternalServerError, err, "failed to set token", rest.ErrInternal, s.templates)
|
rest.SendErrorHTML(w, r, http.StatusInternalServerError, err, "failed to set token", rest.ErrInternal)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,13 +585,13 @@ func (s *private) emailUnsubscribeCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MustRead := func(path string) string {
|
MustRead := func(path string) string {
|
||||||
file, err := s.templates.ReadFile(path)
|
file, err := templates.Read(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return string(file)
|
return string(file)
|
||||||
}
|
}
|
||||||
tmplstr := MustRead("unsubscribe.html.tmpl")
|
tmplstr := MustRead("email_unsubscribe.html.tmpl")
|
||||||
tmpl := template.Must(template.New("unsubscribe").Parse(tmplstr))
|
tmpl := template.Must(template.New("unsubscribe").Parse(tmplstr))
|
||||||
msg := bytes.Buffer{}
|
msg := bytes.Buffer{}
|
||||||
MustExecute(tmpl, &msg, nil)
|
MustExecute(tmpl, &msg, nil)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -24,6 +25,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/umputun/remark42/backend/app/notify"
|
"github.com/umputun/remark42/backend/app/notify"
|
||||||
|
"github.com/umputun/remark42/backend/app/rest/proxy"
|
||||||
"github.com/umputun/remark42/backend/app/store"
|
"github.com/umputun/remark42/backend/app/store"
|
||||||
"github.com/umputun/remark42/backend/app/store/image"
|
"github.com/umputun/remark42/backend/app/store/image"
|
||||||
)
|
)
|
||||||
@@ -54,6 +56,111 @@ func TestRest_Create(t *testing.T) {
|
|||||||
assert.True(t, len(c["id"].(string)) > 8)
|
assert.True(t, len(c["id"].(string)) > 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// based on issue https://github.com/umputun/remark42/issues/1292
|
||||||
|
func TestRest_CreateFilteredCode(t *testing.T) {
|
||||||
|
ts, _, teardown := startupT(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
resp, err := post(t, ts.URL+"/api/v1/comment",
|
||||||
|
`{"text": "`+"`foo<bar>`"+`", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusCreated, resp.StatusCode, string(b))
|
||||||
|
assert.NoError(t, resp.Body.Close())
|
||||||
|
|
||||||
|
c := R.JSON{}
|
||||||
|
err = json.Unmarshal(b, &c)
|
||||||
|
require.NoError(t, err, string(b))
|
||||||
|
loc := c["locator"].(map[string]interface{})
|
||||||
|
assert.Equal(t, "remark42", loc["site"])
|
||||||
|
assert.Equal(t, "https://radio-t.com/blah1", loc["url"])
|
||||||
|
assert.Equal(t, "`foo<bar>`", c["orig"])
|
||||||
|
assert.Contains(t, c["text"], "foo")
|
||||||
|
assert.Contains(t, c["text"], "bar")
|
||||||
|
assert.True(t, len(c["id"].(string)) > 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on issue https://github.com/umputun/remark42/issues/1631
|
||||||
|
func TestRest_CreateAndPreviewWithImage(t *testing.T) {
|
||||||
|
ts, srv, teardown := startupT(t)
|
||||||
|
ts.Close()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
srv.ImageService.ProxyAPI = srv.RemarkURL + "/api/v1/img"
|
||||||
|
srv.ImageProxy = &proxy.Image{
|
||||||
|
HTTP2HTTPS: true,
|
||||||
|
CacheExternal: true,
|
||||||
|
RoutePath: "/api/v1/img",
|
||||||
|
RemarkURL: srv.RemarkURL,
|
||||||
|
ImageService: srv.ImageService,
|
||||||
|
}
|
||||||
|
srv.CommentFormatter = store.NewCommentFormatter(srv.ImageProxy)
|
||||||
|
// need to recreate the server with new ImageProxy, otherwise old one will be used
|
||||||
|
ts = httptest.NewServer(srv.routes())
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
var pngRead bool
|
||||||
|
// server with the test PNG image
|
||||||
|
pngServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, e := io.Copy(w, gopherPNG())
|
||||||
|
assert.NoError(t, e)
|
||||||
|
pngRead = true
|
||||||
|
}))
|
||||||
|
defer pngServer.Close()
|
||||||
|
|
||||||
|
t.Run("create", func(t *testing.T) {
|
||||||
|
resp, err := post(t, ts.URL+"/api/v1/comment",
|
||||||
|
`{"text": "", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusCreated, resp.StatusCode, string(b))
|
||||||
|
assert.NoError(t, resp.Body.Close())
|
||||||
|
|
||||||
|
c := R.JSON{}
|
||||||
|
err = json.Unmarshal(b, &c)
|
||||||
|
require.NoError(t, err, string(b))
|
||||||
|
assert.NotContains(t, c["text"], pngServer.URL)
|
||||||
|
assert.Contains(t, c["text"], srv.RemarkURL)
|
||||||
|
loc := c["locator"].(map[string]interface{})
|
||||||
|
assert.Equal(t, "remark42", loc["site"])
|
||||||
|
assert.Equal(t, "https://radio-t.com/blah1", loc["url"])
|
||||||
|
assert.True(t, len(c["id"].(string)) > 8)
|
||||||
|
assert.Equal(t, false, pngRead, "original image is not yet accessed by server")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("preview", func(t *testing.T) {
|
||||||
|
resp, err := post(t, ts.URL+"/api/v1/preview",
|
||||||
|
`{"text": "", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(b))
|
||||||
|
assert.NoError(t, resp.Body.Close())
|
||||||
|
|
||||||
|
assert.NotContains(t, string(b), pngServer.URL)
|
||||||
|
assert.Contains(t, string(b), srv.RemarkURL)
|
||||||
|
|
||||||
|
assert.Equal(t, false, pngRead, "original image is not yet accessed by server")
|
||||||
|
// retrieve the image from the cache
|
||||||
|
imgURL := strings.Split(strings.Split(string(b), "src=\"")[1], "\"")[0]
|
||||||
|
// replace srv.RemarkURL with ts.URL
|
||||||
|
imgURL = strings.ReplaceAll(imgURL, srv.RemarkURL, ts.URL)
|
||||||
|
resp, err = http.Get(imgURL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
b, err = io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(b))
|
||||||
|
assert.NoError(t, resp.Body.Close())
|
||||||
|
// compare image to original gopher png after decoding from base64
|
||||||
|
assert.Equal(t, gopher, base64.StdEncoding.EncodeToString(b))
|
||||||
|
|
||||||
|
assert.Equal(t, true, pngRead, "original image accessed to be shown to user")
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestRest_CreateOldPost(t *testing.T) {
|
func TestRest_CreateOldPost(t *testing.T) {
|
||||||
ts, srv, teardown := startupT(t)
|
ts, srv, teardown := startupT(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
@@ -141,6 +248,25 @@ func TestRest_CreateWithRestrictedWord(t *testing.T) {
|
|||||||
assert.Equal(t, "invalid comment", c["details"])
|
assert.Equal(t, "invalid comment", c["details"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRest_CreateRelativeURL(t *testing.T) {
|
||||||
|
ts, _, teardown := startupT(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
// check that it's not possible to click insert URL button and not alter the URL in it (which is `url` by default)
|
||||||
|
relativeURLText := `{"text": "here is a link with relative URL: [google.com](url)", "locator":{"url": "https://radio-t.com/blah1", "site": "remark42"}}`
|
||||||
|
resp, err := post(t, ts.URL+"/api/v1/comment", relativeURLText)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, resp.Body.Close())
|
||||||
|
c := R.JSON{}
|
||||||
|
err = json.Unmarshal(b, &c)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "links should start with mailto:, http:// or https://", c["error"])
|
||||||
|
assert.Equal(t, "invalid comment", c["details"])
|
||||||
|
}
|
||||||
|
|
||||||
func TestRest_CreateRejected(t *testing.T) {
|
func TestRest_CreateRejected(t *testing.T) {
|
||||||
ts, _, teardown := startupT(t)
|
ts, _, teardown := startupT(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
@@ -348,6 +474,109 @@ func TestRest_UpdateDelete(t *testing.T) {
|
|||||||
{URL: "https://radio-t.com/blah2", Count: 0}}, j)
|
{URL: "https://radio-t.com/blah2", Count: 0}}, j)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRest_DeleteChildThenParent(t *testing.T) {
|
||||||
|
ts, _, teardown := startupT(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
p := store.Comment{Text: "test test #1",
|
||||||
|
Locator: store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah1"}}
|
||||||
|
idP := addComment(t, p, ts)
|
||||||
|
|
||||||
|
c1 := store.Comment{Text: "test test #1", ParentID: idP,
|
||||||
|
Locator: store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah1"}}
|
||||||
|
idC1 := addComment(t, c1, ts)
|
||||||
|
|
||||||
|
c2 := store.Comment{Text: "test test #1", ParentID: idP,
|
||||||
|
Locator: store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah1"}}
|
||||||
|
idC2 := addComment(t, c2, ts)
|
||||||
|
|
||||||
|
// check multi count equals two
|
||||||
|
resp, err := post(t, ts.URL+"/api/v1/counts?site=remark42", `["https://radio-t.com/blah1"]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
bb, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NoError(t, resp.Body.Close())
|
||||||
|
j := []store.PostInfo{}
|
||||||
|
err = json.Unmarshal(bb, &j)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/blah1", Count: 3}}, j)
|
||||||
|
|
||||||
|
// update a parent comment fails after child is created
|
||||||
|
client := http.Client{}
|
||||||
|
defer client.CloseIdleConnections()
|
||||||
|
req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+idP+"?site=remark42&url=https://radio-t.com/blah1",
|
||||||
|
strings.NewReader(`{"text": "updated text", "summary":"updated by user"}`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Add("X-JWT", devToken)
|
||||||
|
b, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err := io.ReadAll(b.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, b.StatusCode, string(body))
|
||||||
|
assert.NoError(t, b.Body.Close())
|
||||||
|
|
||||||
|
// delete first child comment
|
||||||
|
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+idC1+"?site=remark42&url=https://radio-t.com/blah1",
|
||||||
|
strings.NewReader(`{"delete": true, "summary":"removed by user"}`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Add("X-JWT", devToken)
|
||||||
|
b, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err = io.ReadAll(b.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, b.StatusCode, string(body))
|
||||||
|
assert.NoError(t, b.Body.Close())
|
||||||
|
|
||||||
|
// delete a parent comment, fails as one comment child still present
|
||||||
|
defer client.CloseIdleConnections()
|
||||||
|
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+idP+"?site=remark42&url=https://radio-t.com/blah1",
|
||||||
|
strings.NewReader(`{"delete": true, "summary":"removed by user"}`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Add("X-JWT", devToken)
|
||||||
|
b, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err = io.ReadAll(b.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, b.StatusCode, string(body))
|
||||||
|
assert.NoError(t, b.Body.Close())
|
||||||
|
|
||||||
|
// delete second child comment, as an admin to check both deletion methods
|
||||||
|
req, err = http.NewRequest(http.MethodDelete,
|
||||||
|
fmt.Sprintf("%s/api/v1/admin/comment/%s?site=remark42&url=https://radio-t.com/blah1", ts.URL, idC2), http.NoBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
requireAdminOnly(t, req)
|
||||||
|
resp, err = sendReq(t, req, adminUmputunToken)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, resp.Body.Close())
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// delete a parent comment, shouldn't fail after children are deleted
|
||||||
|
defer client.CloseIdleConnections()
|
||||||
|
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+idP+"?site=remark42&url=https://radio-t.com/blah1",
|
||||||
|
strings.NewReader(`{"delete": true, "summary":"removed by user"}`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Add("X-JWT", devToken)
|
||||||
|
b, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err = io.ReadAll(b.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, b.StatusCode, string(body))
|
||||||
|
assert.NoError(t, b.Body.Close())
|
||||||
|
|
||||||
|
// check multi count decremented to zero
|
||||||
|
resp, err = post(t, ts.URL+"/api/v1/counts?site=remark42", `["https://radio-t.com/blah1"]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
bb, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NoError(t, resp.Body.Close())
|
||||||
|
j = []store.PostInfo{}
|
||||||
|
err = json.Unmarshal(bb, &j)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/blah1", Count: 0}}, j)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRest_UpdateNotOwner(t *testing.T) {
|
func TestRest_UpdateNotOwner(t *testing.T) {
|
||||||
ts, srv, teardown := startupT(t)
|
ts, srv, teardown := startupT(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
@@ -371,8 +600,6 @@ func TestRest_UpdateNotOwner(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusForbidden, b.StatusCode, string(body), "update from non-owner")
|
assert.Equal(t, http.StatusForbidden, b.StatusCode, string(body), "update from non-owner")
|
||||||
assert.Equal(t, `{"code":3,"details":"can not edit comments for other users","error":"rejected"}`+"\n", string(body))
|
assert.Equal(t, `{"code":3,"details":"can not edit comments for other users","error":"rejected"}`+"\n", string(body))
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
defer client.CloseIdleConnections()
|
|
||||||
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+
|
req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+
|
||||||
"?site=remark42&url=https://radio-t.com/blah1", strings.NewReader(`ERRR "text":"updated text", "summary":"my"}`))
|
"?site=remark42&url=https://radio-t.com/blah1", strings.NewReader(`ERRR "text":"updated text", "summary":"my"}`))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -575,17 +802,10 @@ func TestRest_AnonVote(t *testing.T) {
|
|||||||
assert.Equal(t, map[string]store.VotedIPInfo(nil), cr.VotedIPs)
|
assert.Equal(t, map[string]store.VotedIPInfo(nil), cr.VotedIPs)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockFS struct{}
|
|
||||||
|
|
||||||
func (fs *MockFS) ReadFile(path string) ([]byte, error) {
|
|
||||||
return []byte(fmt.Sprintf("template %s", path)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRest_EmailAndTelegram(t *testing.T) {
|
func TestRest_EmailAndTelegram(t *testing.T) {
|
||||||
ts, srv, teardown := startupT(t)
|
ts, srv, teardown := startupT(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
|
|
||||||
srv.privRest.templates = &MockFS{}
|
|
||||||
srv.privRest.telegramService = &mockTelegram{site: "remark42"}
|
srv.privRest.telegramService = &mockTelegram{site: "remark42"}
|
||||||
|
|
||||||
// issue good token
|
// issue good token
|
||||||
@@ -609,19 +829,25 @@ func TestRest_EmailAndTelegram(t *testing.T) {
|
|||||||
responseCode int
|
responseCode int
|
||||||
noAuth bool
|
noAuth bool
|
||||||
cookieEmail string
|
cookieEmail string
|
||||||
|
body string
|
||||||
}{
|
}{
|
||||||
{description: "issue delete request without auth", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusUnauthorized, noAuth: true},
|
{description: "issue delete request without auth", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusUnauthorized, noAuth: true},
|
||||||
{description: "issue delete request without site_id", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusBadRequest},
|
{description: "issue delete request without site_id", url: "/api/v1/email", method: http.MethodDelete, responseCode: http.StatusBadRequest},
|
||||||
{description: "delete non-existent user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
|
{description: "delete non-existent user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
|
||||||
{description: "set user email, token not set", url: "/api/v1/email/confirm?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
{description: "set user email, token not set", url: "/api/v1/email/confirm", method: http.MethodPost, responseCode: http.StatusBadRequest, body: `{"site":"remark42"}`},
|
||||||
{description: "send email confirmation without address", url: "/api/v1/email/subscribe?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
{description: "set user email, token not set, old query param", url: "/api/v1/email/confirm?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
||||||
{description: "send email confirmation", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
|
{description: "send email confirmation without address", url: "/api/v1/email/subscribe", method: http.MethodPost, responseCode: http.StatusBadRequest, body: `{"site":"remark42"}`},
|
||||||
{description: "set user email, token is good", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
|
{description: "send email confirmation without address, old query param", url: "/api/v1/email/subscribe?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
||||||
|
{description: "send email confirmation", url: "/api/v1/email/subscribe", method: http.MethodPost, responseCode: http.StatusOK, body: `{"site":"remark42","address":"good@example.com"}`},
|
||||||
|
{description: "send email confirmation, old query param", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
|
||||||
|
{description: "set user email, token is good", url: "/api/v1/email/confirm", method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com", body: fmt.Sprintf(`{"site":"remark42","token":%q}`, goodToken)},
|
||||||
|
{description: "set user email, token is good, old query param", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
|
||||||
{description: "send confirmation with same address", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusConflict},
|
{description: "send confirmation with same address", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusConflict},
|
||||||
{description: "get user email", url: "/api/v1/email?site=remark42", method: http.MethodGet, responseCode: http.StatusOK},
|
{description: "get user email", url: "/api/v1/email?site=remark42", method: http.MethodGet, responseCode: http.StatusOK},
|
||||||
{description: "delete user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
|
{description: "delete user email", url: "/api/v1/email?site=remark42", method: http.MethodDelete, responseCode: http.StatusOK},
|
||||||
{description: "send another confirmation", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
|
{description: "send another confirmation", url: "/api/v1/email/subscribe?site=remark42&address=good@example.com", method: http.MethodPost, responseCode: http.StatusOK},
|
||||||
{description: "set user email, token is good", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
|
{description: "set user email, token is good", url: "/api/v1/email/confirm", method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com", body: fmt.Sprintf(`{"site":"remark42","token":%q}`, goodToken)},
|
||||||
|
{description: "set user email, token is good, old query param", url: fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK, cookieEmail: "good@example.com"},
|
||||||
{description: "unsubscribe user, no token", url: "/email/unsubscribe.html?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
{description: "unsubscribe user, no token", url: "/email/unsubscribe.html?site=remark42", method: http.MethodPost, responseCode: http.StatusBadRequest},
|
||||||
{description: "unsubscribe user, wrong token", url: "/email/unsubscribe.html?site=remark42&tkn=jwt", method: http.MethodGet, responseCode: http.StatusForbidden},
|
{description: "unsubscribe user, wrong token", url: "/email/unsubscribe.html?site=remark42&tkn=jwt", method: http.MethodGet, responseCode: http.StatusForbidden},
|
||||||
{description: "unsubscribe user, good token", url: fmt.Sprintf("/email/unsubscribe.html?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK},
|
{description: "unsubscribe user, good token", url: fmt.Sprintf("/email/unsubscribe.html?site=remark42&tkn=%s", goodToken), method: http.MethodPost, responseCode: http.StatusOK},
|
||||||
@@ -642,7 +868,11 @@ func TestRest_EmailAndTelegram(t *testing.T) {
|
|||||||
for _, x := range testData {
|
for _, x := range testData {
|
||||||
x := x
|
x := x
|
||||||
t.Run(x.description, func(t *testing.T) {
|
t.Run(x.description, func(t *testing.T) {
|
||||||
req, err := http.NewRequest(x.method, ts.URL+x.url, http.NoBody)
|
reqBody := io.NopCloser(strings.NewReader(x.body))
|
||||||
|
if x.body == "" {
|
||||||
|
reqBody = http.NoBody
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(x.method, ts.URL+x.url, reqBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if !x.noAuth {
|
if !x.noAuth {
|
||||||
req.Header.Add("X-JWT", devToken)
|
req.Header.Add("X-JWT", devToken)
|
||||||
@@ -718,7 +948,11 @@ func TestRest_EmailNotification(t *testing.T) {
|
|||||||
assert.Empty(t, mockDestination.Get()[1].Emails)
|
assert.Empty(t, mockDestination.Get()[1].Emails)
|
||||||
|
|
||||||
// send confirmation token for email
|
// send confirmation token for email
|
||||||
req, err = http.NewRequest(http.MethodPost, ts.URL+"/api/v1/email/subscribe?site=remark42&address=good@example.com", http.NoBody)
|
req, err = http.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
ts.URL+"/api/v1/email/subscribe",
|
||||||
|
io.NopCloser(strings.NewReader(`{"site": "remark42", "address": "good@example.com"}`)),
|
||||||
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Add("X-JWT", devToken)
|
req.Header.Add("X-JWT", devToken)
|
||||||
resp, err = client.Do(req)
|
resp, err = client.Do(req)
|
||||||
@@ -733,8 +967,31 @@ func TestRest_EmailNotification(t *testing.T) {
|
|||||||
assert.Equal(t, "good@example.com", mockDestination.GetVerify()[0].Email)
|
assert.Equal(t, "good@example.com", mockDestination.GetVerify()[0].Email)
|
||||||
verificationToken := mockDestination.GetVerify()[0].Token
|
verificationToken := mockDestination.GetVerify()[0].Token
|
||||||
|
|
||||||
|
// get user information to verify lack of the subscription
|
||||||
|
req, err = http.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
ts.URL+"/api/v1/user?site=remark42",
|
||||||
|
http.NoBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Add("X-JWT", devToken)
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||||
|
var clearUser store.User
|
||||||
|
err = json.Unmarshal(body, &clearUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, store.User{Name: "developer one", ID: "dev", EmailSubscription: false,
|
||||||
|
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, clearUser)
|
||||||
|
|
||||||
// verify email
|
// verify email
|
||||||
req, err = http.NewRequest(http.MethodPost, ts.URL+fmt.Sprintf("/api/v1/email/confirm?site=remark42&tkn=%s", verificationToken), http.NoBody)
|
req, err = http.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
ts.URL+"/api/v1/email/confirm",
|
||||||
|
io.NopCloser(strings.NewReader(fmt.Sprintf(`{"site": "remark42", "token": %q}`, verificationToken))),
|
||||||
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Add("X-JWT", devToken)
|
req.Header.Add("X-JWT", devToken)
|
||||||
resp, err = client.Do(req)
|
resp, err = client.Do(req)
|
||||||
@@ -745,7 +1002,10 @@ func TestRest_EmailNotification(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||||
|
|
||||||
// get user information to verify the subscription
|
// get user information to verify the subscription
|
||||||
req, err = http.NewRequest(http.MethodGet, ts.URL+"/api/v1/user?site=remark42", http.NoBody)
|
req, err = http.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
ts.URL+"/api/v1/user?site=remark42",
|
||||||
|
http.NoBody)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Add("X-JWT", devToken)
|
req.Header.Add("X-JWT", devToken)
|
||||||
resp, err = client.Do(req)
|
resp, err = client.Do(req)
|
||||||
@@ -754,11 +1014,11 @@ func TestRest_EmailNotification(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, resp.Body.Close())
|
require.NoError(t, resp.Body.Close())
|
||||||
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||||
var user store.User
|
var subscribedUser store.User
|
||||||
err = json.Unmarshal(body, &user)
|
err = json.Unmarshal(body, &subscribedUser)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, store.User{Name: "developer one", ID: "dev", EmailSubscription: true,
|
assert.Equal(t, store.User{Name: "developer one", ID: "dev", EmailSubscription: true,
|
||||||
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, user)
|
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, subscribedUser)
|
||||||
|
|
||||||
// create child comment from another user, email notification expected
|
// create child comment from another user, email notification expected
|
||||||
req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(fmt.Sprintf(
|
req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", strings.NewReader(fmt.Sprintf(
|
||||||
@@ -809,6 +1069,80 @@ func TestRest_EmailNotification(t *testing.T) {
|
|||||||
time.Sleep(time.Millisecond * 30)
|
time.Sleep(time.Millisecond * 30)
|
||||||
require.Equal(t, 4, len(mockDestination.Get()))
|
require.Equal(t, 4, len(mockDestination.Get()))
|
||||||
assert.Empty(t, mockDestination.Get()[3].Emails)
|
assert.Empty(t, mockDestination.Get()[3].Emails)
|
||||||
|
|
||||||
|
// confirm email via subscribe call with query params, old behavior, email notification is expected
|
||||||
|
req, err = http.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
ts.URL+"/api/v1/email/subscribe?site=remark42&address=good@example.com",
|
||||||
|
http.NoBody,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Add("X-JWT", emailUserToken)
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||||
|
// wait for mock notification Submit to kick off
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
require.Equal(t, 2, len(mockDestination.GetVerify()), "verification email was sent")
|
||||||
|
|
||||||
|
// get email user information to verify there is no subscription yet
|
||||||
|
req, err = http.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
ts.URL+"/api/v1/user?site=remark42",
|
||||||
|
http.NoBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Add("X-JWT", emailUserToken)
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||||
|
var unsubscribedEmailUser store.User
|
||||||
|
err = json.Unmarshal(body, &unsubscribedEmailUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, store.User{Name: "good@example.com test user", ID: "email_f5dfe9d2e6bd75fc74ea5fabf273b45b5baeb195", EmailSubscription: false,
|
||||||
|
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, unsubscribedEmailUser)
|
||||||
|
|
||||||
|
// confirm email via subscribe call, no email notification is expected
|
||||||
|
req, err = http.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
ts.URL+"/api/v1/email/subscribe",
|
||||||
|
io.NopCloser(strings.NewReader(`{"site": "remark42", "address": "good@example.com"}`)),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Add("X-JWT", emailUserToken)
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||||
|
// wait for mock notification Submit to kick off
|
||||||
|
time.Sleep(time.Millisecond * 30)
|
||||||
|
require.Equal(t, 2, len(mockDestination.GetVerify()), "no new verification email was sent")
|
||||||
|
|
||||||
|
// get email user information to verify the subscription happened without the confirmation call
|
||||||
|
req, err = http.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
ts.URL+"/api/v1/user?site=remark42",
|
||||||
|
http.NoBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Add("X-JWT", emailUserToken)
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(body))
|
||||||
|
var subscribedEmailUser store.User
|
||||||
|
err = json.Unmarshal(body, &subscribedEmailUser)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, store.User{Name: "good@example.com test user", ID: "email_f5dfe9d2e6bd75fc74ea5fabf273b45b5baeb195", EmailSubscription: true,
|
||||||
|
Picture: "http://example.com/pic.png", IP: "127.0.0.1", SiteID: "remark42"}, subscribedEmailUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRest_TelegramNotification(t *testing.T) {
|
func TestRest_TelegramNotification(t *testing.T) {
|
||||||
@@ -904,8 +1238,8 @@ func TestRest_TelegramNotification(t *testing.T) {
|
|||||||
body, err = io.ReadAll(resp.Body)
|
body, err = io.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, resp.Body.Close())
|
require.NoError(t, resp.Body.Close())
|
||||||
require.Equal(t, http.StatusInternalServerError, resp.StatusCode, string(body))
|
require.Equal(t, http.StatusNotFound, resp.StatusCode, string(body))
|
||||||
require.Equal(t, `{"code":0,"details":"can't set telegram for user","error":"not verified"}`+"\n", string(body))
|
require.Equal(t, `{"code":0,"details":"request is not verified yet","error":"not verified"}`+"\n", string(body))
|
||||||
|
|
||||||
mockTlgrm.notVerified = false
|
mockTlgrm.notVerified = false
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -32,7 +30,6 @@ type public struct {
|
|||||||
readOnlyAge int
|
readOnlyAge int
|
||||||
commentFormatter *store.CommentFormatter
|
commentFormatter *store.CommentFormatter
|
||||||
imageService *image.Service
|
imageService *image.Service
|
||||||
webRoot string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type pubStore interface {
|
type pubStore interface {
|
||||||
@@ -43,7 +40,7 @@ type pubStore interface {
|
|||||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||||
UserCount(siteID, userID string) (int, error)
|
UserCount(siteID, userID string) (int, error)
|
||||||
Count(locator store.Locator) (int, error)
|
Count(locator store.Locator) (int, error)
|
||||||
List(siteID string, limit int, skip int) ([]store.PostInfo, error)
|
List(siteID string, limit, skip int) ([]store.PostInfo, error)
|
||||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
||||||
|
|
||||||
ValidateComment(c *store.Comment) error
|
ValidateComment(c *store.Comment) error
|
||||||
@@ -345,16 +342,6 @@ func (s *public) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /index.html - respond to /index.html with the content of getstarted.html under /web root
|
|
||||||
func (s *public) getStartedCtrl(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data, err := os.ReadFile(path.Join(s.webRoot, "getstarted.html"))
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
render.HTML(w, r, string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /robots.txt
|
// GET /robots.txt
|
||||||
func (s *public) robotsCtrl(w http.ResponseWriter, r *http.Request) {
|
func (s *public) robotsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||||
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config", "/user",
|
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config", "/user",
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ func TestRest_Preview(t *testing.T) {
|
|||||||
assert.NoError(t, resp.Body.Close())
|
assert.NoError(t, resp.Body.Close())
|
||||||
assert.Contains(t,
|
assert.Contains(t,
|
||||||
string(b),
|
string(b),
|
||||||
"{\"code\":20,\"details\":\"can't renew staged picture cleanup timer\","+
|
`{"code":20,"details":"can't load picture from the comment",`+
|
||||||
"\"error\":\"can't get image stats for dev_user/bad_picture: stat",
|
`"error":"can't get image stats for dev_user/bad_picture: stat`,
|
||||||
)
|
)
|
||||||
assert.Contains(t,
|
assert.Contains(t,
|
||||||
string(b),
|
string(b),
|
||||||
@@ -97,8 +97,8 @@ func TestRest_PreviewWithWrongImage(t *testing.T) {
|
|||||||
assert.NoError(t, resp.Body.Close())
|
assert.NoError(t, resp.Body.Close())
|
||||||
assert.Contains(t,
|
assert.Contains(t,
|
||||||
string(b),
|
string(b),
|
||||||
"{\"code\":20,\"details\":\"can't renew staged picture cleanup timer\","+
|
`{"code":20,"details":"can't load picture from the comment",`+
|
||||||
"\"error\":\"can't get image stats for dev_user/bad_picture: stat ",
|
`"error":"can't get image stats for dev_user/bad_picture: stat `,
|
||||||
)
|
)
|
||||||
assert.Contains(t,
|
assert.Contains(t,
|
||||||
string(b),
|
string(b),
|
||||||
@@ -131,10 +131,10 @@ BKT
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
`<h1>h1</h1>
|
`<h1>h1</h1>
|
||||||
<pre class="chroma"><code><span><span>func TestRest_Preview(t *testing.T) {
|
<pre class="chroma"><code><span class="line"><span class="cl">func TestRest_Preview(t *testing.T) {
|
||||||
</span></span><span><span>srv, ts := prep(t)
|
</span></span><span class="line"><span class="cl">srv, ts := prep(t)
|
||||||
</span></span><span><span> require.NotNil(t, srv)
|
</span></span><span class="line"><span class="cl"> require.NotNil(t, srv)
|
||||||
</span></span><span><span>}
|
</span></span><span class="line"><span class="cl">}
|
||||||
</span></span></code></pre>`,
|
</span></span></code></pre>`,
|
||||||
string(b))
|
string(b))
|
||||||
assert.NoError(t, resp.Body.Close())
|
assert.NoError(t, resp.Body.Close())
|
||||||
@@ -157,7 +157,7 @@ BKT
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
b, err := io.ReadAll(resp.Body)
|
b, err := io.ReadAll(resp.Body)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, `<pre class="chroma"><code><span><span><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
|
assert.Equal(t, `<pre class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
|
||||||
</span></span></code></pre>`, string(b))
|
</span></span></code></pre>`, string(b))
|
||||||
assert.NoError(t, resp.Body.Close())
|
assert.NoError(t, resp.Body.Close())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,15 @@ import (
|
|||||||
"github.com/umputun/remark42/backend/app/store/service"
|
"github.com/umputun/remark42/backend/app/store/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// To generate a token, enter one of the tokens here into https://jwt.io, change the secret to one you're using in your test
|
||||||
|
// ("secret" in case of startupT), and alter the fields you want to be changed.
|
||||||
|
|
||||||
var devToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg`
|
var devToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg`
|
||||||
|
|
||||||
var anonToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImFub255bW91cyB0ZXN0IHVzZXIiLCJpZCI6ImFub255bW91c190ZXN0X3VzZXIiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoiYW5vbkBleGFtcGxlLmNvbSJ9fQ.gAae2WMxZNZE5ebVboptPEyQ7Nk6EQxciNnGJ_mPOuU`
|
var anonToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImFub255bW91cyB0ZXN0IHVzZXIiLCJpZCI6ImFub255bW91c190ZXN0X3VzZXIiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoiYW5vbkBleGFtcGxlLmNvbSJ9fQ.gAae2WMxZNZE5ebVboptPEyQ7Nk6EQxciNnGJ_mPOuU`
|
||||||
|
|
||||||
|
var emailUserToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6Imdvb2RAZXhhbXBsZS5jb20gdGVzdCB1c2VyIiwiaWQiOiJlbWFpbF9mNWRmZTlkMmU2YmQ3NWZjNzRlYTVmYWJmMjczYjQ1YjViYWViMTk1IiwicGljdHVyZSI6Imh0dHA6Ly9leGFtcGxlLmNvbS9waWMucG5nIiwiaXAiOiIxMjcuMC4wLjEiLCJlbWFpbCI6Imdvb2RAZXhhbXBsZS5jb20ifX0.vH2HN1JpuXL8okTJq1A-zGHQ-l2ILcwxvDDEmu2zwks`
|
||||||
|
|
||||||
var devTokenBadAud = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0Ml9iYWQiLCJleHAiOjM3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTIxODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJkZXZlbG9wZXIgb25lIiwiaWQiOiJkZXYiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoibWVAZXhhbXBsZS5jb20ifX0.FuTTocVtcxr4VjpfIICvU2yOb3su28VkDzj94H9Q3xY`
|
var devTokenBadAud = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0Ml9iYWQiLCJleHAiOjM3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTIxODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJkZXZlbG9wZXIgb25lIiwiaWQiOiJkZXYiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoibWVAZXhhbXBsZS5jb20ifX0.FuTTocVtcxr4VjpfIICvU2yOb3su28VkDzj94H9Q3xY`
|
||||||
|
|
||||||
var adminUmputunToken = `eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6MTk1NDU5Nzk4MCwianRpIjoiOTdhMmUwYWM0ZGM3ZDVmNjkyNmQ1ZTg2MjBhY2VmOWE0MGMwIiwiaWF0IjoxNDU0NTk3NjgwLCJpc3MiOiJyZW1hcms0MiIsInVzZXIiOnsibmFtZSI6IlVtcHV0dW4iLCJpZCI6ImdpdGh1Yl9lZjBmNzA2YTciLCJwaWN0dXJlIjoiaHR0cHM6Ly9yZW1hcms0Mi5yYWRpby10LmNvbS9hcGkvdjEvYXZhdGFyL2NiNDJmZjQ5M2FkZTY5NmQ4OGEzYTU5MGYxMzZhZTllMzRkZTdjMWIuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.dZiOjWHguo9f42XCMooMcv4EmYFzifl_-LEvPZHCtks`
|
var adminUmputunToken = `eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6MTk1NDU5Nzk4MCwianRpIjoiOTdhMmUwYWM0ZGM3ZDVmNjkyNmQ1ZTg2MjBhY2VmOWE0MGMwIiwiaWF0IjoxNDU0NTk3NjgwLCJpc3MiOiJyZW1hcms0MiIsInVzZXIiOnsibmFtZSI6IlVtcHV0dW4iLCJpZCI6ImdpdGh1Yl9lZjBmNzA2YTciLCJwaWN0dXJlIjoiaHR0cHM6Ly9yZW1hcms0Mi5yYWRpby10LmNvbS9hcGkvdjEvYXZhdGFyL2NiNDJmZjQ5M2FkZTY5NmQ4OGEzYTU5MGYxMzZhZTllMzRkZTdjMWIuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.dZiOjWHguo9f42XCMooMcv4EmYFzifl_-LEvPZHCtks`
|
||||||
@@ -60,23 +65,6 @@ func TestRest_FileServer(t *testing.T) {
|
|||||||
_ = os.Remove(testHTMLFile)
|
_ = os.Remove(testHTMLFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRest_GetStarted(t *testing.T) {
|
|
||||||
ts, _, teardown := startupT(t)
|
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
getStartedHTML := os.TempDir() + "/getstarted.html"
|
|
||||||
err := os.WriteFile(getStartedHTML, []byte("some html blah"), 0o700)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
body, code := get(t, ts.URL+"/index.html")
|
|
||||||
assert.Equal(t, http.StatusOK, code)
|
|
||||||
assert.Equal(t, "some html blah", body)
|
|
||||||
|
|
||||||
_ = os.Remove(getStartedHTML)
|
|
||||||
_, code = get(t, ts.URL+"/index.html")
|
|
||||||
assert.Equal(t, http.StatusNotFound, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRest_Shutdown(t *testing.T) {
|
func TestRest_Shutdown(t *testing.T) {
|
||||||
srv := Rest{Authenticator: &auth.Service{}, ImageProxy: &proxy.Image{}}
|
srv := Rest{Authenticator: &auth.Service{}, ImageProxy: &proxy.Image{}}
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
@@ -313,7 +301,7 @@ func TestRest_cacheControl(t *testing.T) {
|
|||||||
for i, tt := range tbl {
|
for i, tt := range tbl {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
req := httptest.NewRequest("GET", tt.url, nil)
|
req := httptest.NewRequest("GET", tt.url, http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
h := cacheControl(tt.exp, tt.version)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
h := cacheControl(tt.exp, tt.version)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||||
@@ -341,7 +329,7 @@ func TestRest_frameAncestors(t *testing.T) {
|
|||||||
for i, tt := range tbl {
|
for i, tt := range tbl {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
req := httptest.NewRequest("GET", "http://example.com", http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
h := frameAncestors(tt.hosts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
h := frameAncestors(tt.hosts)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||||
@@ -375,7 +363,7 @@ func TestRest_subscribersOnly(t *testing.T) {
|
|||||||
for i, tt := range tbl {
|
for i, tt := range tbl {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
req := httptest.NewRequest("GET", "http://example.com", http.NoBody)
|
||||||
if tt.setUser {
|
if tt.setUser {
|
||||||
req = token.SetUserInfo(req, tt.user)
|
req = token.SetUserInfo(req, tt.user)
|
||||||
}
|
}
|
||||||
@@ -389,12 +377,13 @@ func TestRest_subscribersOnly(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_validEmaiAuth(t *testing.T) {
|
func Test_validEmailAuth(t *testing.T) {
|
||||||
tbl := []struct {
|
tbl := []struct {
|
||||||
req string
|
req string
|
||||||
status int
|
status int
|
||||||
}{
|
}{
|
||||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=someone", http.StatusOK},
|
{"/auth/email/login?site=remark42&address=umputun%example.com&user=someone", http.StatusOK},
|
||||||
|
{"/auth/email/login?site=site-with-dash_and_underscore-and.dot&address=umputun%example.com&user=someone", http.StatusOK},
|
||||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=someone+blah", http.StatusOK},
|
{"/auth/email/login?site=remark42&address=umputun%example.com&user=someone+blah", http.StatusOK},
|
||||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=Евгений+Умпутун", http.StatusOK},
|
{"/auth/email/login?site=remark42&address=umputun%example.com&user=Евгений+Умпутун", http.StatusOK},
|
||||||
{"/auth/email/login?site=remark42&address=umputun%example.com&user=12", http.StatusForbidden},
|
{"/auth/email/login?site=remark42&address=umputun%example.com&user=12", http.StatusForbidden},
|
||||||
@@ -411,7 +400,7 @@ func Test_validEmaiAuth(t *testing.T) {
|
|||||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||||
req := httptest.NewRequest("GET", "http://example.com"+tt.req, http.NoBody)
|
req := httptest.NewRequest("GET", "http://example.com"+tt.req, http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
h := validEmaiAuth()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
h := validEmailAuth()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||||
h.ServeHTTP(w, req)
|
h.ServeHTTP(w, req)
|
||||||
resp := w.Result()
|
resp := w.Result()
|
||||||
assert.Equal(t, tt.status, resp.StatusCode)
|
assert.Equal(t, tt.status, resp.StatusCode)
|
||||||
|
|||||||
@@ -111,15 +111,11 @@ func (s *Rest) getRemarkHost() string {
|
|||||||
|
|
||||||
func (s *Rest) makeTLSConfig() *tls.Config {
|
func (s *Rest) makeTLSConfig() *tls.Config {
|
||||||
return &tls.Config{
|
return &tls.Config{
|
||||||
PreferServerCipherSuites: true,
|
|
||||||
CipherSuites: []uint16{
|
CipherSuites: []uint16{
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
// tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
||||||
// tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
// tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
|
||||||
},
|
},
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
CurvePreferences: []tls.CurveID{
|
CurvePreferences: []tls.CurveID{
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ type errTmplData struct {
|
|||||||
Details string
|
Details string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendErrorHTML makes html body with provided template and responds with provided http status code,
|
// SendErrorHTML makes html body from error_response.html.tmpl template and responds with provided http status code,
|
||||||
// error code is not included in render as it is intended for UI developers and not for the users
|
// error code is not included in render as it is intended for UI developers and not for the users
|
||||||
func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, err error, details string, errCode int, t templates.FileReader) {
|
func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, err error, details string, errCode int) {
|
||||||
// MustExecute behaves like template.Execute, but panics if an error occurs.
|
// MustExecute behaves like template.Execute, but panics if an error occurs.
|
||||||
MustExecute := func(tmpl *template.Template, wr io.Writer, data interface{}) {
|
MustExecute := func(tmpl *template.Template, wr io.Writer, data interface{}) {
|
||||||
if err = tmpl.Execute(wr, data); err != nil {
|
if err = tmpl.Execute(wr, data); err != nil {
|
||||||
@@ -58,7 +58,7 @@ func SendErrorHTML(w http.ResponseWriter, r *http.Request, httpStatusCode int, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MustRead := func(path string) string {
|
MustRead := func(path string) string {
|
||||||
file, e := t.ReadFile(path)
|
file, e := templates.Read(path)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
panic(e)
|
panic(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,18 +36,11 @@ func TestSendErrorJSON(t *testing.T) {
|
|||||||
assert.Equal(t, `{"code":123,"details":"error details 123456","error":"error 500"}`+"\n", string(body))
|
assert.Equal(t, `{"code":123,"details":"error details 123456","error":"error 500"}`+"\n", string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockFS struct{}
|
|
||||||
|
|
||||||
func (fs *MockFS) ReadFile(path string) ([]byte, error) {
|
|
||||||
return []byte(fmt.Sprintf("{{.Error}}{{.Details}} %s", path)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendErrorHTML(t *testing.T) {
|
func TestSendErrorHTML(t *testing.T) {
|
||||||
fs := &MockFS{}
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/error" {
|
if r.URL.Path == "/error" {
|
||||||
t.Log("http err request", r.URL)
|
t.Log("http err request", r.URL)
|
||||||
SendErrorHTML(w, r, 500, fmt.Errorf("error 500"), "error details 123456", 987, fs)
|
SendErrorHTML(w, r, 500, fmt.Errorf("error 500"), "error details 123456", 987)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(404)
|
w.WriteHeader(404)
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func (p Image) downloadImage(ctx context.Context, imgURL string) ([]byte, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can't download image %s: %w", imgURL, err)
|
return nil, fmt.Errorf("can't download image %s: %w", imgURL, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close() //nolint gosec // we don't care about response body
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("got unsuccessful response status %d while fetching %s", resp.StatusCode, imgURL)
|
return nil, fmt.Errorf("got unsuccessful response status %d while fetching %s", resp.StatusCode, imgURL)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/umputun/remark42/backend/app/store/image"
|
"github.com/umputun/remark42/backend/app/store/image"
|
||||||
@@ -95,7 +94,8 @@ func TestImage_Replace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImage_Routes(t *testing.T) {
|
func TestImage_Routes(t *testing.T) {
|
||||||
imageStore := image.MockStore{}
|
// no image supposed to be cached
|
||||||
|
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) { return nil, nil }}
|
||||||
img := Image{
|
img := Image{
|
||||||
HTTP2HTTPS: true,
|
HTTP2HTTPS: true,
|
||||||
RemarkURL: "https://demo.remark42.com",
|
RemarkURL: "https://demo.remark42.com",
|
||||||
@@ -110,9 +110,6 @@ func TestImage_Routes(t *testing.T) {
|
|||||||
|
|
||||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
||||||
|
|
||||||
// no image supposed to be cached
|
|
||||||
imageStore.On("Load", mock.Anything).Times(2).Return(nil, nil)
|
|
||||||
|
|
||||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NoError(t, resp.Body.Close())
|
assert.NoError(t, resp.Body.Close())
|
||||||
@@ -131,10 +128,11 @@ func TestImage_Routes(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NoError(t, resp.Body.Close())
|
assert.NoError(t, resp.Body.Close())
|
||||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
assert.Equal(t, 2, len(imageStore.LoadCalls()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
|
func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
|
||||||
imageStore := image.MockStore{}
|
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) { return nil, nil }}
|
||||||
img := Image{
|
img := Image{
|
||||||
RemarkURL: "https://demo.remark42.com",
|
RemarkURL: "https://demo.remark42.com",
|
||||||
RoutePath: "/api/v1/proxy",
|
RoutePath: "/api/v1/proxy",
|
||||||
@@ -148,8 +146,6 @@ func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
|
|||||||
|
|
||||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
||||||
|
|
||||||
imageStore.On("Load", mock.Anything).Once().Return(nil, nil)
|
|
||||||
|
|
||||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NoError(t, resp.Body.Close())
|
assert.NoError(t, resp.Body.Close())
|
||||||
@@ -157,11 +153,18 @@ func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {
|
|||||||
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
|
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
|
||||||
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
|
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
|
||||||
|
|
||||||
imageStore.AssertCalled(t, "Load", mock.Anything)
|
assert.Equal(t, 1, len(imageStore.LoadCalls()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImage_RoutesCachingImage(t *testing.T) {
|
func TestImage_RoutesCachingImage(t *testing.T) {
|
||||||
imageStore := image.MockStore{}
|
imageStore := image.StoreMock{
|
||||||
|
LoadFunc: func(id string) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
SaveFunc: func(id string, img []byte) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
img := Image{
|
img := Image{
|
||||||
CacheExternal: true,
|
CacheExternal: true,
|
||||||
RemarkURL: "https://demo.remark42.com",
|
RemarkURL: "https://demo.remark42.com",
|
||||||
@@ -177,9 +180,6 @@ func TestImage_RoutesCachingImage(t *testing.T) {
|
|||||||
imgURL := httpSrv.URL + "/image/img1.png"
|
imgURL := httpSrv.URL + "/image/img1.png"
|
||||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(imgURL))
|
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(imgURL))
|
||||||
|
|
||||||
imageStore.On("Load", mock.Anything).Once().Return(nil, nil)
|
|
||||||
imageStore.On("Save", mock.Anything, mock.Anything).Once().Return(nil)
|
|
||||||
|
|
||||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NoError(t, resp.Body.Close())
|
assert.NoError(t, resp.Body.Close())
|
||||||
@@ -187,12 +187,18 @@ func TestImage_RoutesCachingImage(t *testing.T) {
|
|||||||
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
|
assert.Equal(t, "1462", resp.Header["Content-Length"][0])
|
||||||
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
|
assert.Equal(t, "image/png", resp.Header["Content-Type"][0])
|
||||||
|
|
||||||
imageStore.AssertCalled(t, "Load", mock.Anything)
|
assert.Equal(t, 1, len(imageStore.LoadCalls()))
|
||||||
imageStore.AssertCalled(t, "Save", "cached_images/4b84b15bff6ee5796152495a230e45e3d7e947d9-"+image.Sha1Str(imgURL), gopherPNGBytes())
|
assert.Equal(t, 1, len(imageStore.SaveCalls()))
|
||||||
|
assert.Equal(t, "cached_images/4b84b15bff6ee5796152495a230e45e3d7e947d9-"+image.Sha1Str(imgURL), imageStore.SaveCalls()[0].ID)
|
||||||
|
assert.Equal(t, gopherPNGBytes(), imageStore.SaveCalls()[0].Img)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
||||||
imageStore := image.MockStore{}
|
// In order to validate that cached data used cache "will return" some other data from what http server would
|
||||||
|
testImage := []byte(fmt.Sprintf("%256s", "X"))
|
||||||
|
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) {
|
||||||
|
return testImage, nil
|
||||||
|
}}
|
||||||
img := Image{
|
img := Image{
|
||||||
CacheExternal: true,
|
CacheExternal: true,
|
||||||
RemarkURL: "https://demo.remark42.com",
|
RemarkURL: "https://demo.remark42.com",
|
||||||
@@ -207,10 +213,6 @@ func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
|||||||
|
|
||||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png"))
|
||||||
|
|
||||||
// In order to validate that cached data used cache "will return" some other data from what http server would
|
|
||||||
testImage := []byte(fmt.Sprintf("%256s", "X"))
|
|
||||||
imageStore.On("Load", mock.Anything).Once().Return(testImage, nil)
|
|
||||||
|
|
||||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NoError(t, resp.Body.Close())
|
assert.NoError(t, resp.Body.Close())
|
||||||
@@ -219,11 +221,12 @@ func TestImage_RoutesUsingCachedImage(t *testing.T) {
|
|||||||
assert.Equal(t, "text/plain; charset=utf-8", resp.Header["Content-Type"][0],
|
assert.Equal(t, "text/plain; charset=utf-8", resp.Header["Content-Type"][0],
|
||||||
"if you save text you receive text/plain in response, that's only fair option you got")
|
"if you save text you receive text/plain in response, that's only fair option you got")
|
||||||
|
|
||||||
imageStore.AssertCalled(t, "Load", mock.Anything)
|
assert.Equal(t, 1, len(imageStore.LoadCalls()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImage_RoutesTimedOut(t *testing.T) {
|
func TestImage_RoutesTimedOut(t *testing.T) {
|
||||||
imageStore := image.MockStore{}
|
// no image supposed to be cached
|
||||||
|
imageStore := image.StoreMock{LoadFunc: func(id string) ([]byte, error) { return nil, nil }}
|
||||||
img := Image{
|
img := Image{
|
||||||
HTTP2HTTPS: true,
|
HTTP2HTTPS: true,
|
||||||
RemarkURL: "https://demo.remark42.com",
|
RemarkURL: "https://demo.remark42.com",
|
||||||
@@ -239,9 +242,6 @@ func TestImage_RoutesTimedOut(t *testing.T) {
|
|||||||
|
|
||||||
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img-slow.png"))
|
encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img-slow.png"))
|
||||||
|
|
||||||
// no image supposed to be cached
|
|
||||||
imageStore.On("Load", mock.Anything).Once().Return(nil, nil)
|
|
||||||
|
|
||||||
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||||
@@ -250,6 +250,7 @@ func TestImage_RoutesTimedOut(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Log(string(b))
|
t.Log(string(b))
|
||||||
assert.True(t, strings.Contains(string(b), "deadline exceeded"))
|
assert.True(t, strings.Contains(string(b), "deadline exceeded"))
|
||||||
|
assert.Equal(t, 1, len(imageStore.LoadCalls()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImage_ConvertProxyMode(t *testing.T) {
|
func TestImage_ConvertProxyMode(t *testing.T) {
|
||||||
|
|||||||
@@ -78,5 +78,5 @@ func (s *StaticStore) Enabled(site string) (ok bool, err error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnEvent doesn nothing for StaticStore
|
// OnEvent does nothing for StaticStore
|
||||||
func (s *StaticStore) OnEvent(_ string, _ EventType) error { return nil }
|
func (s *StaticStore) OnEvent(_ string, _ EventType) error { return nil }
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Comment struct {
|
|||||||
ID string `json:"id" bson:"_id"`
|
ID string `json:"id" bson:"_id"`
|
||||||
ParentID string `json:"pid"`
|
ParentID string `json:"pid"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Orig string `json:"orig,omitempty"`
|
Orig string `json:"orig,omitempty"` // important: never render this as HTML! It's not sanitized.
|
||||||
User User `json:"user"`
|
User User `json:"user"`
|
||||||
Locator Locator `json:"locator"`
|
Locator Locator `json:"locator"`
|
||||||
Score int `json:"score"`
|
Score int `json:"score"`
|
||||||
@@ -112,20 +112,23 @@ func (c *Comment) SetDeleted(mode DeleteMode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize clean dangerous html/js from the comment
|
// Sanitize clean dangerous html/js from the comment.
|
||||||
|
// Comment.Orig which is used to store the original comment text is not sanitized
|
||||||
|
// as we expect to never render it as HTML and render Comment.Text instead
|
||||||
func (c *Comment) Sanitize() {
|
func (c *Comment) Sanitize() {
|
||||||
p := bluemonday.UGCPolicy()
|
p := bluemonday.UGCPolicy()
|
||||||
p.AllowAttrs("class").Matching(regexp.MustCompile("^chroma$")).OnElements("pre")
|
p.AllowAttrs("class").Matching(regexp.MustCompile("^chroma$")).OnElements("pre")
|
||||||
|
// special case for embedding the quotes from Twitter
|
||||||
|
p.AllowAttrs("class").Matching(regexp.MustCompile("^twitter-tweet$")).OnElements("blockquote")
|
||||||
// this is list of <span> tag classes which could be produced by chroma code renderer
|
// this is list of <span> tag classes which could be produced by chroma code renderer
|
||||||
// source: https://github.com/alecthomas/chroma/blob/022b6f4fc2c4aa819aac18363c8de3f70619200b/types.go#L221-L316
|
// source: https://github.com/alecthomas/chroma/blob/c263f6f/types.go#L209-L306
|
||||||
const codeSpanClassRegex = "^(chroma|ln|lnt|hl|lntable|lntd|w|err|x|esc|k|kc" +
|
const codeSpanClassRegex = "^(bg|chroma|line|ln|lnt|hl|lntable|lntd|lnlinks|cl|w|err|x|k|kc" +
|
||||||
"|kd|kn|kp|kr|kt|n|na|nb|bp|nc|no|nd|ni|ne|nf|fm|py|nl|nn|nx|nt|nv|vc|vg" +
|
"|kd|kn|kp|kr|kt|n|na|nb|bp|nc|no|nd|ni|ne|nf|fm|py|nl|nn|nx|nt|nv|vc|vg" +
|
||||||
"|vi|vm|l|ld|s|sa|sb|sc|dl|sd|s2|se|sh|si|sx|sr|s1|ss|m|mb|mf|mh|mi|il" +
|
"|vi|vm|l|ld|s|sa|sb|sc|dl|sd|s2|se|sh|si|sx|sr|s1|ss|m|mb|mf|mh|mi|il" +
|
||||||
"|mo|o|ow|p|c|ch|cm|cp|cpf|c1|cs|g|gd|ge|gr|gh|gi|go|gp|gs|gu|gt|gl)$"
|
"|mo|o|ow|p|c|ch|cm|cp|cpf|c1|cs|g|gd|ge|gr|gh|gi|go|gp|gs|gu|gt|gl)$"
|
||||||
p.AllowAttrs("class").Matching(regexp.MustCompile(codeSpanClassRegex)).OnElements("span")
|
p.AllowAttrs("class").Matching(regexp.MustCompile(codeSpanClassRegex)).OnElements("span")
|
||||||
p.AllowAttrs("loading").Matching(regexp.MustCompile("^(lazy|eager)$")).OnElements("img")
|
p.AllowAttrs("loading").Matching(regexp.MustCompile("^(lazy|eager)$")).OnElements("img")
|
||||||
c.Text = p.Sanitize(c.Text)
|
c.Text = p.Sanitize(c.Text)
|
||||||
c.Orig = p.Sanitize(c.Orig)
|
|
||||||
c.User.ID = template.HTMLEscapeString(c.User.ID)
|
c.User.ID = template.HTMLEscapeString(c.User.ID)
|
||||||
c.User.Name = c.SanitizeText(c.User.Name)
|
c.User.Name = c.SanitizeText(c.User.Name)
|
||||||
c.User.Picture = c.SanitizeAsURL(c.User.Picture)
|
c.User.Picture = c.SanitizeAsURL(c.User.Picture)
|
||||||
@@ -143,7 +146,7 @@ func (c *Comment) Snippet(limit int) string {
|
|||||||
if size < limit {
|
if size < limit {
|
||||||
return cleanText
|
return cleanText
|
||||||
}
|
}
|
||||||
snippet := []rune(cleanText)[:size]
|
snippet := []rune(cleanText)[:limit]
|
||||||
// go back in snippet and found the first space
|
// go back in snippet and found the first space
|
||||||
for i := len(snippet) - 1; i >= 0; i-- {
|
for i := len(snippet) - 1; i >= 0; i-- {
|
||||||
if snippet[i] == ' ' {
|
if snippet[i] == ' ' {
|
||||||
@@ -151,6 +154,10 @@ func (c *Comment) Snippet(limit int) string {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Don't add a space if comment is just a one single word which has been truncated.
|
||||||
|
if len(snippet) == limit {
|
||||||
|
return string(snippet) + "..."
|
||||||
|
}
|
||||||
return string(snippet) + " ..."
|
return string(snippet) + " ..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ func TestComment_Sanitize(t *testing.T) {
|
|||||||
inp: Comment{Text: "blah blah", PostTitle: "<script>alert()</script>something"},
|
inp: Comment{Text: "blah blah", PostTitle: "<script>alert()</script>something"},
|
||||||
out: Comment{Text: "blah blah", PostTitle: "something"},
|
out: Comment{Text: "blah blah", PostTitle: "something"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
inp: Comment{Text: `<blockquote class="twitter-tweet"><p lang="es" dir="ltr">Silicon iMac Concept<a href="https://t.co/7ga95QxVXn">https://t.co/7ga95QxVXn</a> by <a href="https://twitter.com/marcsheep?ref_src=twsrc%5Etfw">@marcsheep</a> <a href="https://t.co/ULnVpG8w55">pic.twitter.com/ULnVpG8w55</a></p>— Andreas Storm (@avstorm) <a href="https://twitter.com/avstorm/status/1325693387798933504?ref_src=twsrc%5Etfw">November 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`, PostTitle: "Twitter quote"},
|
||||||
|
out: Comment{Text: `<blockquote class="twitter-tweet"><p lang="es" dir="ltr">Silicon iMac Concept<a href="https://t.co/7ga95QxVXn" rel="nofollow">https://t.co/7ga95QxVXn</a> by <a href="https://twitter.com/marcsheep?ref_src=twsrc%5Etfw" rel="nofollow">@marcsheep</a> <a href="https://t.co/ULnVpG8w55" rel="nofollow">pic.twitter.com/ULnVpG8w55</a></p>— Andreas Storm (@avstorm) <a href="https://twitter.com/avstorm/status/1325693387798933504?ref_src=twsrc%5Etfw" rel="nofollow">November 9, 2020</a></blockquote> `, PostTitle: "Twitter quote"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for n, tt := range tbl {
|
for n, tt := range tbl {
|
||||||
@@ -192,8 +196,8 @@ func TestComment_Snippet(t *testing.T) {
|
|||||||
{0, "", ""},
|
{0, "", ""},
|
||||||
{-1, "test\nblah", "test blah"},
|
{-1, "test\nblah", "test blah"},
|
||||||
{5, "test\nblah", "test ..."},
|
{5, "test\nblah", "test ..."},
|
||||||
{5, "xyz12345 xxx", "xyz12345 ..."},
|
{5, "xyz12345 xxx", "xyz12..."},
|
||||||
{10, "xyz12345 xxx\ntest 123456", "xyz12345 xxx test ..."},
|
{10, "xyz12345 xxx\ntest 123456", "xyz12345 ..."},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tt := range tbl {
|
for i, tt := range tbl {
|
||||||
@@ -259,7 +263,7 @@ func TestComment_sanitizeText(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"<a href=javascript:alert(document.domain)//>xxx</a>",
|
"<a href=javascript:alert(document.domain)//>xxx</a>",
|
||||||
"<a/>xxx</a>",
|
"xxx</a>",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ import (
|
|||||||
|
|
||||||
// BoltDB implements store.Interface, represents multiple sites with multiplexing to different bolt dbs. Thread safe.
|
// BoltDB implements store.Interface, represents multiple sites with multiplexing to different bolt dbs. Thread safe.
|
||||||
// there are 6 types of top-level buckets:
|
// there are 6 types of top-level buckets:
|
||||||
// - comments for post in "posts" top-level bucket. Each url (post) makes its own bucket and each k:v pair is commentID:comment
|
// - comments for post in "posts" top-level bucket. Each url (post) makes its own bucket and each k:v pair is commentID:comment
|
||||||
// - history of all comments. They all in a single "last" bucket (per site) and key is defined by ref struct as ts+commentID
|
// - history of all comments. They all in a single "last" bucket (per site) and key is defined by ref struct as ts+commentID
|
||||||
// value is not full comment but a reference combined from post-url+commentID
|
// value is not full comment but a reference combined from post-url+commentID
|
||||||
// - user to comment references in "users" bucket. It used to get comments for user. Key is userID and value
|
// - user to comment references in "users" bucket. It used to get comments for user. Key is userID and value
|
||||||
// is a nested bucket named userID with kv as ts:reference
|
// is a nested bucket named userID with kv as ts:reference
|
||||||
// - users details in "user_details" bucket. Key is userID, value - UserDetailEntry
|
// - users details in "user_details" bucket. Key is userID, value - UserDetailEntry
|
||||||
// - blocking info sits in "block" bucket. Key is userID, value - ts
|
// - blocking info sits in "block" bucket. Key is userID, value - ts
|
||||||
// - counts per post to keep number of comments. Key is post url, value - count
|
// - counts per post to keep number of comments. Key is post url, value - count
|
||||||
// - readonly per post to keep status of manually set RO posts. Key is post url, value - ts
|
// - readonly per post to keep status of manually set RO posts. Key is post url, value - ts
|
||||||
type BoltDB struct {
|
type BoltDB struct {
|
||||||
dbs map[string]*bolt.DB
|
dbs map[string]*bolt.DB
|
||||||
}
|
}
|
||||||
@@ -780,7 +780,7 @@ func (b *BoltDB) deleteUserDetail(bdb *bolt.DB, userID string, userDetail UserDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
return bdb.Update(func(tx *bolt.Tx) error {
|
return bdb.Update(func(tx *bolt.Tx) error {
|
||||||
// updated entry is not empty and we need to store it's updated copy
|
// updated entry is not empty and we need to store its updated copy
|
||||||
err := b.save(tx.Bucket([]byte(userDetailsBucketName)), userID, entry)
|
err := b.save(tx.Bucket([]byte(userDetailsBucketName)), userID, entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update detail %s for %s: %w", userDetail, userID, err)
|
return fmt.Errorf("failed to update detail %s for %s: %w", userDetail, userID, err)
|
||||||
|
|||||||
@@ -911,6 +911,13 @@ func TestBoltDB_NewFailed(t *testing.T) {
|
|||||||
assert.EqualError(t, err, "failed to make boltdb for /tmp/no-such-place/tmp.db: open /tmp/no-such-place/tmp.db: no such file or directory")
|
assert.EqualError(t, err, "failed to make boltdb for /tmp/no-such-place/tmp.db: open /tmp/no-such-place/tmp.db: no such file or directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBoltDB_DoubleClose(t *testing.T) {
|
||||||
|
var b, teardown = prep(t)
|
||||||
|
defer teardown()
|
||||||
|
assert.NoError(t, b.Close())
|
||||||
|
assert.NoError(t, b.Close(), "second call should not result in panic or errors")
|
||||||
|
}
|
||||||
|
|
||||||
// makes new boltdb, put two records
|
// makes new boltdb, put two records
|
||||||
func prep(t *testing.T) (b *BoltDB, teardown func()) {
|
func prep(t *testing.T) (b *BoltDB, teardown func()) {
|
||||||
_ = os.Remove(testDB)
|
_ = os.Remove(testDB)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"github.com/umputun/remark42/backend/app/store"
|
"github.com/umputun/remark42/backend/app/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NOTE: mockery works from linked to go-path and with GOFLAGS='-mod=vendor' go generate
|
// NOTE: matryer/moq should be installed globally and works with `go generate ./...`
|
||||||
//go:generate sh -c "mockery -inpkg -name Interface -print > /tmp/engine-mock.tmp && mv /tmp/engine-mock.tmp engine_mock.go"
|
//go:generate moq --out engine_mock.go . Interface
|
||||||
|
|
||||||
// Interface defines methods provided by low-level storage engine
|
// Interface defines methods provided by low-level storage engine
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
|
|||||||
@@ -1,231 +1,508 @@
|
|||||||
// Code generated by mockery v1.1.2. DO NOT EDIT.
|
// Code generated by moq; DO NOT EDIT.
|
||||||
|
// github.com/matryer/moq
|
||||||
|
|
||||||
package engine
|
package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
mock "github.com/stretchr/testify/mock"
|
|
||||||
store "github.com/umputun/remark42/backend/app/store"
|
store "github.com/umputun/remark42/backend/app/store"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockInterface is an autogenerated mock type for the Interface type
|
// Ensure, that InterfaceMock does implement Interface.
|
||||||
type MockInterface struct {
|
// If this is not the case, regenerate this file with moq.
|
||||||
mock.Mock
|
var _ Interface = &InterfaceMock{}
|
||||||
}
|
|
||||||
|
|
||||||
// Close provides a mock function with given fields:
|
// InterfaceMock is a mock implementation of Interface.
|
||||||
func (_m *MockInterface) Close() error {
|
//
|
||||||
ret := _m.Called()
|
// func TestSomethingThatUsesInterface(t *testing.T) {
|
||||||
|
//
|
||||||
|
// // make and configure a mocked Interface
|
||||||
|
// mockedInterface := &InterfaceMock{
|
||||||
|
// CloseFunc: func() error {
|
||||||
|
// panic("mock out the Close method")
|
||||||
|
// },
|
||||||
|
// CountFunc: func(req FindRequest) (int, error) {
|
||||||
|
// panic("mock out the Count method")
|
||||||
|
// },
|
||||||
|
// CreateFunc: func(comment store.Comment) (string, error) {
|
||||||
|
// panic("mock out the Create method")
|
||||||
|
// },
|
||||||
|
// DeleteFunc: func(req DeleteRequest) error {
|
||||||
|
// panic("mock out the Delete method")
|
||||||
|
// },
|
||||||
|
// FindFunc: func(req FindRequest) ([]store.Comment, error) {
|
||||||
|
// panic("mock out the Find method")
|
||||||
|
// },
|
||||||
|
// FlagFunc: func(req FlagRequest) (bool, error) {
|
||||||
|
// panic("mock out the Flag method")
|
||||||
|
// },
|
||||||
|
// GetFunc: func(req GetRequest) (store.Comment, error) {
|
||||||
|
// panic("mock out the Get method")
|
||||||
|
// },
|
||||||
|
// InfoFunc: func(req InfoRequest) ([]store.PostInfo, error) {
|
||||||
|
// panic("mock out the Info method")
|
||||||
|
// },
|
||||||
|
// ListFlagsFunc: func(req FlagRequest) ([]interface{}, error) {
|
||||||
|
// panic("mock out the ListFlags method")
|
||||||
|
// },
|
||||||
|
// UpdateFunc: func(comment store.Comment) error {
|
||||||
|
// panic("mock out the Update method")
|
||||||
|
// },
|
||||||
|
// UserDetailFunc: func(req UserDetailRequest) ([]UserDetailEntry, error) {
|
||||||
|
// panic("mock out the UserDetail method")
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // use mockedInterface in code that requires Interface
|
||||||
|
// // and then make assertions.
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
type InterfaceMock struct {
|
||||||
|
// CloseFunc mocks the Close method.
|
||||||
|
CloseFunc func() error
|
||||||
|
|
||||||
var r0 error
|
// CountFunc mocks the Count method.
|
||||||
if rf, ok := ret.Get(0).(func() error); ok {
|
CountFunc func(req FindRequest) (int, error)
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
// CreateFunc mocks the Create method.
|
||||||
}
|
CreateFunc func(comment store.Comment) (string, error)
|
||||||
|
|
||||||
// Count provides a mock function with given fields: req
|
// DeleteFunc mocks the Delete method.
|
||||||
func (_m *MockInterface) Count(req FindRequest) (int, error) {
|
DeleteFunc func(req DeleteRequest) error
|
||||||
ret := _m.Called(req)
|
|
||||||
|
|
||||||
var r0 int
|
// FindFunc mocks the Find method.
|
||||||
if rf, ok := ret.Get(0).(func(FindRequest) int); ok {
|
FindFunc func(req FindRequest) ([]store.Comment, error)
|
||||||
r0 = rf(req)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(int)
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
// FlagFunc mocks the Flag method.
|
||||||
if rf, ok := ret.Get(1).(func(FindRequest) error); ok {
|
FlagFunc func(req FlagRequest) (bool, error)
|
||||||
r1 = rf(req)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
// GetFunc mocks the Get method.
|
||||||
}
|
GetFunc func(req GetRequest) (store.Comment, error)
|
||||||
|
|
||||||
// Create provides a mock function with given fields: comment
|
// InfoFunc mocks the Info method.
|
||||||
func (_m *MockInterface) Create(comment store.Comment) (string, error) {
|
InfoFunc func(req InfoRequest) ([]store.PostInfo, error)
|
||||||
ret := _m.Called(comment)
|
|
||||||
|
|
||||||
var r0 string
|
// ListFlagsFunc mocks the ListFlags method.
|
||||||
if rf, ok := ret.Get(0).(func(store.Comment) string); ok {
|
ListFlagsFunc func(req FlagRequest) ([]interface{}, error)
|
||||||
r0 = rf(comment)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
// UpdateFunc mocks the Update method.
|
||||||
if rf, ok := ret.Get(1).(func(store.Comment) error); ok {
|
UpdateFunc func(comment store.Comment) error
|
||||||
r1 = rf(comment)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
// UserDetailFunc mocks the UserDetail method.
|
||||||
}
|
UserDetailFunc func(req UserDetailRequest) ([]UserDetailEntry, error)
|
||||||
|
|
||||||
// Delete provides a mock function with given fields: req
|
// calls tracks calls to the methods.
|
||||||
func (_m *MockInterface) Delete(req DeleteRequest) error {
|
calls struct {
|
||||||
ret := _m.Called(req)
|
// Close holds details about calls to the Close method.
|
||||||
|
Close []struct {
|
||||||
var r0 error
|
}
|
||||||
if rf, ok := ret.Get(0).(func(DeleteRequest) error); ok {
|
// Count holds details about calls to the Count method.
|
||||||
r0 = rf(req)
|
Count []struct {
|
||||||
} else {
|
// Req is the req argument value.
|
||||||
r0 = ret.Error(0)
|
Req FindRequest
|
||||||
}
|
}
|
||||||
|
// Create holds details about calls to the Create method.
|
||||||
return r0
|
Create []struct {
|
||||||
}
|
// Comment is the comment argument value.
|
||||||
|
Comment store.Comment
|
||||||
// Find provides a mock function with given fields: req
|
}
|
||||||
func (_m *MockInterface) Find(req FindRequest) ([]store.Comment, error) {
|
// Delete holds details about calls to the Delete method.
|
||||||
ret := _m.Called(req)
|
Delete []struct {
|
||||||
|
// Req is the req argument value.
|
||||||
var r0 []store.Comment
|
Req DeleteRequest
|
||||||
if rf, ok := ret.Get(0).(func(FindRequest) []store.Comment); ok {
|
}
|
||||||
r0 = rf(req)
|
// Find holds details about calls to the Find method.
|
||||||
} else {
|
Find []struct {
|
||||||
if ret.Get(0) != nil {
|
// Req is the req argument value.
|
||||||
r0 = ret.Get(0).([]store.Comment)
|
Req FindRequest
|
||||||
|
}
|
||||||
|
// Flag holds details about calls to the Flag method.
|
||||||
|
Flag []struct {
|
||||||
|
// Req is the req argument value.
|
||||||
|
Req FlagRequest
|
||||||
|
}
|
||||||
|
// Get holds details about calls to the Get method.
|
||||||
|
Get []struct {
|
||||||
|
// Req is the req argument value.
|
||||||
|
Req GetRequest
|
||||||
|
}
|
||||||
|
// Info holds details about calls to the Info method.
|
||||||
|
Info []struct {
|
||||||
|
// Req is the req argument value.
|
||||||
|
Req InfoRequest
|
||||||
|
}
|
||||||
|
// ListFlags holds details about calls to the ListFlags method.
|
||||||
|
ListFlags []struct {
|
||||||
|
// Req is the req argument value.
|
||||||
|
Req FlagRequest
|
||||||
|
}
|
||||||
|
// Update holds details about calls to the Update method.
|
||||||
|
Update []struct {
|
||||||
|
// Comment is the comment argument value.
|
||||||
|
Comment store.Comment
|
||||||
|
}
|
||||||
|
// UserDetail holds details about calls to the UserDetail method.
|
||||||
|
UserDetail []struct {
|
||||||
|
// Req is the req argument value.
|
||||||
|
Req UserDetailRequest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lockClose sync.RWMutex
|
||||||
var r1 error
|
lockCount sync.RWMutex
|
||||||
if rf, ok := ret.Get(1).(func(FindRequest) error); ok {
|
lockCreate sync.RWMutex
|
||||||
r1 = rf(req)
|
lockDelete sync.RWMutex
|
||||||
} else {
|
lockFind sync.RWMutex
|
||||||
r1 = ret.Error(1)
|
lockFlag sync.RWMutex
|
||||||
}
|
lockGet sync.RWMutex
|
||||||
|
lockInfo sync.RWMutex
|
||||||
return r0, r1
|
lockListFlags sync.RWMutex
|
||||||
|
lockUpdate sync.RWMutex
|
||||||
|
lockUserDetail sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flag provides a mock function with given fields: req
|
// Close calls CloseFunc.
|
||||||
func (_m *MockInterface) Flag(req FlagRequest) (bool, error) {
|
func (mock *InterfaceMock) Close() error {
|
||||||
ret := _m.Called(req)
|
if mock.CloseFunc == nil {
|
||||||
|
panic("InterfaceMock.CloseFunc: method is nil but Interface.Close was just called")
|
||||||
var r0 bool
|
|
||||||
if rf, ok := ret.Get(0).(func(FlagRequest) bool); ok {
|
|
||||||
r0 = rf(req)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(bool)
|
|
||||||
}
|
}
|
||||||
|
callInfo := struct {
|
||||||
var r1 error
|
}{}
|
||||||
if rf, ok := ret.Get(1).(func(FlagRequest) error); ok {
|
mock.lockClose.Lock()
|
||||||
r1 = rf(req)
|
mock.calls.Close = append(mock.calls.Close, callInfo)
|
||||||
} else {
|
mock.lockClose.Unlock()
|
||||||
r1 = ret.Error(1)
|
return mock.CloseFunc()
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get provides a mock function with given fields: req
|
// CloseCalls gets all the calls that were made to Close.
|
||||||
func (_m *MockInterface) Get(req GetRequest) (store.Comment, error) {
|
// Check the length with:
|
||||||
ret := _m.Called(req)
|
//
|
||||||
|
// len(mockedInterface.CloseCalls())
|
||||||
var r0 store.Comment
|
func (mock *InterfaceMock) CloseCalls() []struct {
|
||||||
if rf, ok := ret.Get(0).(func(GetRequest) store.Comment); ok {
|
} {
|
||||||
r0 = rf(req)
|
var calls []struct {
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(store.Comment)
|
|
||||||
}
|
}
|
||||||
|
mock.lockClose.RLock()
|
||||||
var r1 error
|
calls = mock.calls.Close
|
||||||
if rf, ok := ret.Get(1).(func(GetRequest) error); ok {
|
mock.lockClose.RUnlock()
|
||||||
r1 = rf(req)
|
return calls
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info provides a mock function with given fields: req
|
// Count calls CountFunc.
|
||||||
func (_m *MockInterface) Info(req InfoRequest) ([]store.PostInfo, error) {
|
func (mock *InterfaceMock) Count(req FindRequest) (int, error) {
|
||||||
ret := _m.Called(req)
|
if mock.CountFunc == nil {
|
||||||
|
panic("InterfaceMock.CountFunc: method is nil but Interface.Count was just called")
|
||||||
var r0 []store.PostInfo
|
|
||||||
if rf, ok := ret.Get(0).(func(InfoRequest) []store.PostInfo); ok {
|
|
||||||
r0 = rf(req)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]store.PostInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
callInfo := struct {
|
||||||
var r1 error
|
Req FindRequest
|
||||||
if rf, ok := ret.Get(1).(func(InfoRequest) error); ok {
|
}{
|
||||||
r1 = rf(req)
|
Req: req,
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
}
|
||||||
|
mock.lockCount.Lock()
|
||||||
return r0, r1
|
mock.calls.Count = append(mock.calls.Count, callInfo)
|
||||||
|
mock.lockCount.Unlock()
|
||||||
|
return mock.CountFunc(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListFlags provides a mock function with given fields: req
|
// CountCalls gets all the calls that were made to Count.
|
||||||
func (_m *MockInterface) ListFlags(req FlagRequest) ([]interface{}, error) {
|
// Check the length with:
|
||||||
ret := _m.Called(req)
|
//
|
||||||
|
// len(mockedInterface.CountCalls())
|
||||||
var r0 []interface{}
|
func (mock *InterfaceMock) CountCalls() []struct {
|
||||||
if rf, ok := ret.Get(0).(func(FlagRequest) []interface{}); ok {
|
Req FindRequest
|
||||||
r0 = rf(req)
|
} {
|
||||||
} else {
|
var calls []struct {
|
||||||
if ret.Get(0) != nil {
|
Req FindRequest
|
||||||
r0 = ret.Get(0).([]interface{})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
mock.lockCount.RLock()
|
||||||
var r1 error
|
calls = mock.calls.Count
|
||||||
if rf, ok := ret.Get(1).(func(FlagRequest) error); ok {
|
mock.lockCount.RUnlock()
|
||||||
r1 = rf(req)
|
return calls
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update provides a mock function with given fields: comment
|
// Create calls CreateFunc.
|
||||||
func (_m *MockInterface) Update(comment store.Comment) error {
|
func (mock *InterfaceMock) Create(comment store.Comment) (string, error) {
|
||||||
ret := _m.Called(comment)
|
if mock.CreateFunc == nil {
|
||||||
|
panic("InterfaceMock.CreateFunc: method is nil but Interface.Create was just called")
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(store.Comment) error); ok {
|
|
||||||
r0 = rf(comment)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
}
|
||||||
|
callInfo := struct {
|
||||||
return r0
|
Comment store.Comment
|
||||||
|
}{
|
||||||
|
Comment: comment,
|
||||||
|
}
|
||||||
|
mock.lockCreate.Lock()
|
||||||
|
mock.calls.Create = append(mock.calls.Create, callInfo)
|
||||||
|
mock.lockCreate.Unlock()
|
||||||
|
return mock.CreateFunc(comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserDetail provides a mock function with given fields: req
|
// CreateCalls gets all the calls that were made to Create.
|
||||||
func (_m *MockInterface) UserDetail(req UserDetailRequest) ([]UserDetailEntry, error) {
|
// Check the length with:
|
||||||
ret := _m.Called(req)
|
//
|
||||||
|
// len(mockedInterface.CreateCalls())
|
||||||
var r0 []UserDetailEntry
|
func (mock *InterfaceMock) CreateCalls() []struct {
|
||||||
if rf, ok := ret.Get(0).(func(UserDetailRequest) []UserDetailEntry); ok {
|
Comment store.Comment
|
||||||
r0 = rf(req)
|
} {
|
||||||
} else {
|
var calls []struct {
|
||||||
if ret.Get(0) != nil {
|
Comment store.Comment
|
||||||
r0 = ret.Get(0).([]UserDetailEntry)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
mock.lockCreate.RLock()
|
||||||
|
calls = mock.calls.Create
|
||||||
|
mock.lockCreate.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
var r1 error
|
// Delete calls DeleteFunc.
|
||||||
if rf, ok := ret.Get(1).(func(UserDetailRequest) error); ok {
|
func (mock *InterfaceMock) Delete(req DeleteRequest) error {
|
||||||
r1 = rf(req)
|
if mock.DeleteFunc == nil {
|
||||||
} else {
|
panic("InterfaceMock.DeleteFunc: method is nil but Interface.Delete was just called")
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Req DeleteRequest
|
||||||
|
}{
|
||||||
|
Req: req,
|
||||||
|
}
|
||||||
|
mock.lockDelete.Lock()
|
||||||
|
mock.calls.Delete = append(mock.calls.Delete, callInfo)
|
||||||
|
mock.lockDelete.Unlock()
|
||||||
|
return mock.DeleteFunc(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCalls gets all the calls that were made to Delete.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedInterface.DeleteCalls())
|
||||||
|
func (mock *InterfaceMock) DeleteCalls() []struct {
|
||||||
|
Req DeleteRequest
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Req DeleteRequest
|
||||||
|
}
|
||||||
|
mock.lockDelete.RLock()
|
||||||
|
calls = mock.calls.Delete
|
||||||
|
mock.lockDelete.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find calls FindFunc.
|
||||||
|
func (mock *InterfaceMock) Find(req FindRequest) ([]store.Comment, error) {
|
||||||
|
if mock.FindFunc == nil {
|
||||||
|
panic("InterfaceMock.FindFunc: method is nil but Interface.Find was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Req FindRequest
|
||||||
|
}{
|
||||||
|
Req: req,
|
||||||
|
}
|
||||||
|
mock.lockFind.Lock()
|
||||||
|
mock.calls.Find = append(mock.calls.Find, callInfo)
|
||||||
|
mock.lockFind.Unlock()
|
||||||
|
return mock.FindFunc(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindCalls gets all the calls that were made to Find.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedInterface.FindCalls())
|
||||||
|
func (mock *InterfaceMock) FindCalls() []struct {
|
||||||
|
Req FindRequest
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Req FindRequest
|
||||||
|
}
|
||||||
|
mock.lockFind.RLock()
|
||||||
|
calls = mock.calls.Find
|
||||||
|
mock.lockFind.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
return r0, r1
|
// Flag calls FlagFunc.
|
||||||
|
func (mock *InterfaceMock) Flag(req FlagRequest) (bool, error) {
|
||||||
|
if mock.FlagFunc == nil {
|
||||||
|
panic("InterfaceMock.FlagFunc: method is nil but Interface.Flag was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Req FlagRequest
|
||||||
|
}{
|
||||||
|
Req: req,
|
||||||
|
}
|
||||||
|
mock.lockFlag.Lock()
|
||||||
|
mock.calls.Flag = append(mock.calls.Flag, callInfo)
|
||||||
|
mock.lockFlag.Unlock()
|
||||||
|
return mock.FlagFunc(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagCalls gets all the calls that were made to Flag.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedInterface.FlagCalls())
|
||||||
|
func (mock *InterfaceMock) FlagCalls() []struct {
|
||||||
|
Req FlagRequest
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Req FlagRequest
|
||||||
|
}
|
||||||
|
mock.lockFlag.RLock()
|
||||||
|
calls = mock.calls.Flag
|
||||||
|
mock.lockFlag.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get calls GetFunc.
|
||||||
|
func (mock *InterfaceMock) Get(req GetRequest) (store.Comment, error) {
|
||||||
|
if mock.GetFunc == nil {
|
||||||
|
panic("InterfaceMock.GetFunc: method is nil but Interface.Get was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Req GetRequest
|
||||||
|
}{
|
||||||
|
Req: req,
|
||||||
|
}
|
||||||
|
mock.lockGet.Lock()
|
||||||
|
mock.calls.Get = append(mock.calls.Get, callInfo)
|
||||||
|
mock.lockGet.Unlock()
|
||||||
|
return mock.GetFunc(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCalls gets all the calls that were made to Get.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedInterface.GetCalls())
|
||||||
|
func (mock *InterfaceMock) GetCalls() []struct {
|
||||||
|
Req GetRequest
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Req GetRequest
|
||||||
|
}
|
||||||
|
mock.lockGet.RLock()
|
||||||
|
calls = mock.calls.Get
|
||||||
|
mock.lockGet.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info calls InfoFunc.
|
||||||
|
func (mock *InterfaceMock) Info(req InfoRequest) ([]store.PostInfo, error) {
|
||||||
|
if mock.InfoFunc == nil {
|
||||||
|
panic("InterfaceMock.InfoFunc: method is nil but Interface.Info was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Req InfoRequest
|
||||||
|
}{
|
||||||
|
Req: req,
|
||||||
|
}
|
||||||
|
mock.lockInfo.Lock()
|
||||||
|
mock.calls.Info = append(mock.calls.Info, callInfo)
|
||||||
|
mock.lockInfo.Unlock()
|
||||||
|
return mock.InfoFunc(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoCalls gets all the calls that were made to Info.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedInterface.InfoCalls())
|
||||||
|
func (mock *InterfaceMock) InfoCalls() []struct {
|
||||||
|
Req InfoRequest
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Req InfoRequest
|
||||||
|
}
|
||||||
|
mock.lockInfo.RLock()
|
||||||
|
calls = mock.calls.Info
|
||||||
|
mock.lockInfo.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFlags calls ListFlagsFunc.
|
||||||
|
func (mock *InterfaceMock) ListFlags(req FlagRequest) ([]interface{}, error) {
|
||||||
|
if mock.ListFlagsFunc == nil {
|
||||||
|
panic("InterfaceMock.ListFlagsFunc: method is nil but Interface.ListFlags was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Req FlagRequest
|
||||||
|
}{
|
||||||
|
Req: req,
|
||||||
|
}
|
||||||
|
mock.lockListFlags.Lock()
|
||||||
|
mock.calls.ListFlags = append(mock.calls.ListFlags, callInfo)
|
||||||
|
mock.lockListFlags.Unlock()
|
||||||
|
return mock.ListFlagsFunc(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFlagsCalls gets all the calls that were made to ListFlags.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedInterface.ListFlagsCalls())
|
||||||
|
func (mock *InterfaceMock) ListFlagsCalls() []struct {
|
||||||
|
Req FlagRequest
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Req FlagRequest
|
||||||
|
}
|
||||||
|
mock.lockListFlags.RLock()
|
||||||
|
calls = mock.calls.ListFlags
|
||||||
|
mock.lockListFlags.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update calls UpdateFunc.
|
||||||
|
func (mock *InterfaceMock) Update(comment store.Comment) error {
|
||||||
|
if mock.UpdateFunc == nil {
|
||||||
|
panic("InterfaceMock.UpdateFunc: method is nil but Interface.Update was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Comment store.Comment
|
||||||
|
}{
|
||||||
|
Comment: comment,
|
||||||
|
}
|
||||||
|
mock.lockUpdate.Lock()
|
||||||
|
mock.calls.Update = append(mock.calls.Update, callInfo)
|
||||||
|
mock.lockUpdate.Unlock()
|
||||||
|
return mock.UpdateFunc(comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCalls gets all the calls that were made to Update.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedInterface.UpdateCalls())
|
||||||
|
func (mock *InterfaceMock) UpdateCalls() []struct {
|
||||||
|
Comment store.Comment
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Comment store.Comment
|
||||||
|
}
|
||||||
|
mock.lockUpdate.RLock()
|
||||||
|
calls = mock.calls.Update
|
||||||
|
mock.lockUpdate.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDetail calls UserDetailFunc.
|
||||||
|
func (mock *InterfaceMock) UserDetail(req UserDetailRequest) ([]UserDetailEntry, error) {
|
||||||
|
if mock.UserDetailFunc == nil {
|
||||||
|
panic("InterfaceMock.UserDetailFunc: method is nil but Interface.UserDetail was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
Req UserDetailRequest
|
||||||
|
}{
|
||||||
|
Req: req,
|
||||||
|
}
|
||||||
|
mock.lockUserDetail.Lock()
|
||||||
|
mock.calls.UserDetail = append(mock.calls.UserDetail, callInfo)
|
||||||
|
mock.lockUserDetail.Unlock()
|
||||||
|
return mock.UserDetailFunc(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDetailCalls gets all the calls that were made to UserDetail.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedInterface.UserDetailCalls())
|
||||||
|
func (mock *InterfaceMock) UserDetailCalls() []struct {
|
||||||
|
Req UserDetailRequest
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
Req UserDetailRequest
|
||||||
|
}
|
||||||
|
mock.lockUserDetail.RLock()
|
||||||
|
calls = mock.calls.UserDetail
|
||||||
|
mock.lockUserDetail.RUnlock()
|
||||||
|
return calls
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,14 +71,44 @@ func (r *RPC) Flag(req FlagRequest) (status bool, err error) {
|
|||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unmarshalString(data []byte) ([]interface{}, error) {
|
||||||
|
var strings []string
|
||||||
|
if err := json.Unmarshal(data, &strings); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list := make([]interface{}, 0, len(strings))
|
||||||
|
for _, w := range strings {
|
||||||
|
list = append(list, w)
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalBlockedUser(data []byte) ([]interface{}, error) {
|
||||||
|
var blockedUsers []store.BlockedUser
|
||||||
|
if err := json.Unmarshal(data, &blockedUsers); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list := make([]interface{}, 0, len(blockedUsers))
|
||||||
|
for _, w := range blockedUsers {
|
||||||
|
list = append(list, w)
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListFlags get list of flagged keys, like blocked & verified user
|
// ListFlags get list of flagged keys, like blocked & verified user
|
||||||
func (r *RPC) ListFlags(req FlagRequest) (list []interface{}, err error) {
|
func (r *RPC) ListFlags(req FlagRequest) ([]interface{}, error) {
|
||||||
resp, err := r.Call("store.list_flags", req)
|
resp, err := r.Call("store.list_flags", req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(*resp.Result, &list)
|
// try []store.BlockedUser
|
||||||
return list, err
|
list, err := unmarshalBlockedUser(*resp.Result)
|
||||||
|
if err == nil {
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try []strings
|
||||||
|
return unmarshalString(*resp.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserDetail sets or gets single detail value, or gets all details for requested site.
|
// UserDetail sets or gets single detail value, or gets all details for requested site.
|
||||||
|
|||||||
@@ -134,7 +134,13 @@ func TestRemote_ListFlag(t *testing.T) {
|
|||||||
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
|
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
|
||||||
res, err := c.ListFlags(FlagRequest{Locator: store.Locator{SiteID: "site_id"}, Flag: Blocked})
|
res, err := c.ListFlags(FlagRequest{Locator: store.Locator{SiteID: "site_id"}, Flag: Blocked})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []interface{}{map[string]interface{}{"ID": "id1"}, map[string]interface{}{"ID": "id2"}}, res)
|
require.Equal(t, 2, len(res))
|
||||||
|
blockedUsers := make([]store.BlockedUser, 0, len(res))
|
||||||
|
for _, v := range res {
|
||||||
|
blockedUsers = append(blockedUsers, v.(store.BlockedUser))
|
||||||
|
}
|
||||||
|
assert.Equal(t, 2, len(blockedUsers))
|
||||||
|
assert.Equal(t, []store.BlockedUser{{ID: "id1"}, {ID: "id2"}}, blockedUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemote_UserDetail(t *testing.T) {
|
func TestRemote_UserDetail(t *testing.T) {
|
||||||
@@ -180,11 +186,17 @@ func TestRemote_Delete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRemote_Close(t *testing.T) {
|
func TestRemote_Close(t *testing.T) {
|
||||||
ts := testServer(t, `{"method":"store.close","id":1}`, `{}`)
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(body), "{\"method\":\"store.close\",\"id\":")
|
||||||
|
t.Logf("req: %s", string(body))
|
||||||
|
_, _ = fmt.Fprint(w, "{}")
|
||||||
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
|
c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
|
||||||
err := c.Close()
|
assert.NoError(t, c.Close())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, c.Close(), "second call should not result in panic or errors")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testServer(t *testing.T, req, resp string) *httptest.Server {
|
func testServer(t *testing.T, req, resp string) *httptest.Server {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Depado/bfchroma"
|
"github.com/Depado/bfchroma/v2"
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/alecthomas/chroma/formatters/html"
|
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||||
bf "github.com/russross/blackfriday/v2"
|
bf "github.com/russross/blackfriday/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,17 +42,8 @@ func (f *CommentFormatter) Format(c Comment) Comment {
|
|||||||
|
|
||||||
// FormatText converts text with markdown processor, applies external converters and shortens links
|
// FormatText converts text with markdown processor, applies external converters and shortens links
|
||||||
func (f *CommentFormatter) FormatText(txt string) (res string) {
|
func (f *CommentFormatter) FormatText(txt string) (res string) {
|
||||||
mdExt := bf.NoIntraEmphasis | bf.Tables | bf.FencedCode |
|
mdExt, rend := GetMdExtensionsAndRenderer()
|
||||||
bf.Strikethrough | bf.SpaceHeadings | bf.HardLineBreak |
|
res = string(bf.Run([]byte(txt), bf.WithExtensions(mdExt), bf.WithRenderer(rend)))
|
||||||
bf.BackslashLineBreak | bf.Autolink
|
|
||||||
|
|
||||||
rend := bf.NewHTMLRenderer(bf.HTMLRendererParameters{
|
|
||||||
Flags: bf.Smartypants | bf.SmartypantsFractions | bf.SmartypantsDashes | bf.SmartypantsAngledQuotes,
|
|
||||||
})
|
|
||||||
|
|
||||||
extRend := bfchroma.NewRenderer(bfchroma.Extend(rend), bfchroma.ChromaOptions(html.WithClasses(true)))
|
|
||||||
|
|
||||||
res = string(bf.Run([]byte(txt), bf.WithExtensions(mdExt), bf.WithRenderer(extRend)))
|
|
||||||
res = f.unEscape(res)
|
res = f.unEscape(res)
|
||||||
|
|
||||||
for _, conv := range f.converters {
|
for _, conv := range f.converters {
|
||||||
@@ -126,3 +117,18 @@ func (f *CommentFormatter) lazyImage(commentHTML string) (resHTML string) {
|
|||||||
}
|
}
|
||||||
return resHTML
|
return resHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMdExtensionsAndRenderer returns blackfriday extensions and renderer used for rendering markdown
|
||||||
|
// within store module.
|
||||||
|
func GetMdExtensionsAndRenderer() (bf.Extensions, *bfchroma.Renderer) {
|
||||||
|
mdExt := bf.NoIntraEmphasis | bf.Tables | bf.FencedCode |
|
||||||
|
bf.Strikethrough | bf.SpaceHeadings | bf.HardLineBreak |
|
||||||
|
bf.BackslashLineBreak | bf.Autolink
|
||||||
|
|
||||||
|
rend := bf.NewHTMLRenderer(bf.HTMLRendererParameters{
|
||||||
|
Flags: bf.Smartypants | bf.SmartypantsFractions | bf.SmartypantsDashes | bf.SmartypantsAngledQuotes,
|
||||||
|
})
|
||||||
|
|
||||||
|
extRend := bfchroma.NewRenderer(bfchroma.Extend(rend), bfchroma.ChromaOptions(html.WithClasses(true)))
|
||||||
|
return mdExt, extRend
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ func TestFormatter_FormatText(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{"— not translated #354", "<p>— not translated #354</p>\n!converted", "mdash"},
|
{"— not translated #354", "<p>— not translated #354</p>\n!converted", "mdash"},
|
||||||
{"smth\n```go\nfunc main(aa string) int {return 0}\n```", `<p>smth</p>
|
{"smth\n```go\nfunc main(aa string) int {return 0}\n```", `<p>smth</p>
|
||||||
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
|
<pre class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">(</span><span class="nx">aa</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span><span class="k">return</span> <span class="mi">0</span><span class="p">}</span>
|
||||||
</span></span></code></pre>!converted`, "code with language"},
|
</span></span></code></pre>!converted`, "code with language"},
|
||||||
{"```\ntest_code\n```", `<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">test_code
|
{"```\ntest_code\n```", `<pre class="chroma"><code><span class="line"><span class="cl">test_code
|
||||||
</span></span></code></pre>!converted`, "code without language"},
|
</span></span></code></pre>!converted`, "code without language"},
|
||||||
}
|
}
|
||||||
f := NewCommentFormatter(mockConverter{})
|
f := NewCommentFormatter(mockConverter{})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package image
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -150,7 +149,7 @@ func checkBoltImgData(t *testing.T, db *bolt.DB, bucket, id string, callback fun
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prepareBoltImageStorageTest(t *testing.T) (svc *Bolt, teardown func()) {
|
func prepareBoltImageStorageTest(t *testing.T) (svc *Bolt, teardown func()) {
|
||||||
loc, err := ioutil.TempDir("", "test_image_r42")
|
loc, err := os.MkdirTemp("", "test_image_r42")
|
||||||
require.NoError(t, err, "failed to make temp dir")
|
require.NoError(t, err, "failed to make temp dir")
|
||||||
|
|
||||||
svc, err = NewBoltStorage(path.Join(loc, "picture.db"), bolt.Options{})
|
svc, err = NewBoltStorage(path.Join(loc, "picture.db"), bolt.Options{})
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -181,7 +180,7 @@ func TestFsStore_Cleanup(t *testing.T) {
|
|||||||
svc, teardown := prepareImageTest(t)
|
svc, teardown := prepareImageTest(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
|
|
||||||
save := func(file string, user string) (filePath string) {
|
save := func(file, user string) (filePath string) {
|
||||||
id := path.Join(user, file)
|
id := path.Join(user, file)
|
||||||
err := svc.Save(id, gopherPNGBytes())
|
err := svc.Save(id, gopherPNGBytes())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -228,7 +227,7 @@ func TestFsStore_Cleanup(t *testing.T) {
|
|||||||
_, err = os.Stat(img2)
|
_, err = os.Stat(img2)
|
||||||
assert.Error(t, err, "no file on staging anymore")
|
assert.Error(t, err, "no file on staging anymore")
|
||||||
_, err = os.Stat(img3)
|
_, err = os.Stat(img3)
|
||||||
assert.NoError(t, err, "third image is still on staging because it's cleanup timer was reset")
|
assert.NoError(t, err, "third image is still on staging because its cleanup timer was reset")
|
||||||
|
|
||||||
err = svc.ResetCleanupTimer("unknown_image.png")
|
err = svc.ResetCleanupTimer("unknown_image.png")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -254,10 +253,10 @@ func TestFsStore_Info(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prepareImageTest(t *testing.T) (svc *FileSystem, teardown func()) {
|
func prepareImageTest(t *testing.T) (svc *FileSystem, teardown func()) {
|
||||||
loc, err := ioutil.TempDir("", "test_image_r42")
|
loc, err := os.MkdirTemp("", "test_image_r42")
|
||||||
require.NoError(t, err, "failed to make temp dir")
|
require.NoError(t, err, "failed to make temp dir")
|
||||||
|
|
||||||
staging, err := ioutil.TempDir("", "test_image_r42.staging")
|
staging, err := os.MkdirTemp("", "test_image_r42.staging")
|
||||||
require.NoError(t, err, "failed to make temp staging dir")
|
require.NoError(t, err, "failed to make temp staging dir")
|
||||||
|
|
||||||
svc = &FileSystem{
|
svc = &FileSystem{
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
// Service object encloses Store and add common methods, this is the one consumer should use.
|
// Service object encloses Store and add common methods, this is the one consumer should use.
|
||||||
package image
|
package image
|
||||||
|
|
||||||
//go:generate sh -c "mockery -inpkg -name Store -print > /tmp/mock.tmp && mv /tmp/mock.tmp image_mock.go"
|
// NOTE: matryer/moq should be installed globally and works with `go generate ./...`
|
||||||
|
//go:generate moq --out image_mock.go . Store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -61,9 +62,6 @@ type StoreInfo struct {
|
|||||||
FirstStagingImageTS time.Time
|
FirstStagingImageTS time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// To regenerate mock run from this directory:
|
|
||||||
// sh -c "mockery -inpkg -name Store -print > /tmp/image-mock.tmp && mv /tmp/image-mock.tmp image_mock.go"
|
|
||||||
|
|
||||||
// Store defines interface for saving and loading pictures.
|
// Store defines interface for saving and loading pictures.
|
||||||
// Declares two-stage save with Commit. Save stores to staging area and Commit moves to the final location.
|
// Declares two-stage save with Commit. Save stores to staging area and Commit moves to the final location.
|
||||||
// Two-stage commit scheme is used for not storing images which are uploaded but later never used in the comments,
|
// Two-stage commit scheme is used for not storing images which are uploaded but later never used in the comments,
|
||||||
@@ -90,8 +88,8 @@ func NewService(s Store, p ServiceParams) *Service {
|
|||||||
return &Service{ServiceParams: p, store: s}
|
return &Service{ServiceParams: p, store: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubmitAndCommit multiple ids immediately
|
// Commit multiple ids immediately
|
||||||
func (s *Service) SubmitAndCommit(idsFn func() []string) error {
|
func (s *Service) Commit(idsFn func() []string) error {
|
||||||
errs := new(multierror.Error)
|
errs := new(multierror.Error)
|
||||||
for _, id := range idsFn() {
|
for _, id := range idsFn() {
|
||||||
err := s.store.Commit(id)
|
err := s.store.Commit(id)
|
||||||
@@ -119,7 +117,7 @@ func (s *Service) Submit(idsFn func() []string) {
|
|||||||
for atomic.LoadInt32(&s.term) == 0 && time.Since(req.TS) <= s.EditDuration {
|
for atomic.LoadInt32(&s.term) == 0 && time.Since(req.TS) <= s.EditDuration {
|
||||||
time.Sleep(time.Millisecond * 10) // small sleep to relive busy wait but keep reactive for term (close)
|
time.Sleep(time.Millisecond * 10) // small sleep to relive busy wait but keep reactive for term (close)
|
||||||
}
|
}
|
||||||
err := s.SubmitAndCommit(req.idsFn)
|
err := s.Commit(req.idsFn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARN] image commit error %v", err)
|
log.Printf("[WARN] image commit error %v", err)
|
||||||
}
|
}
|
||||||
@@ -143,39 +141,14 @@ func (s *Service) Submit(idsFn func() []string) {
|
|||||||
|
|
||||||
// ExtractPictures gets list of images from the doc html and convert from urls to ids, i.e. user/pic.png
|
// ExtractPictures gets list of images from the doc html and convert from urls to ids, i.e. user/pic.png
|
||||||
func (s *Service) ExtractPictures(commentHTML string) (ids []string) {
|
func (s *Service) ExtractPictures(commentHTML string) (ids []string) {
|
||||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(commentHTML))
|
return s.extractImageIDs(commentHTML, true)
|
||||||
if err != nil {
|
}
|
||||||
log.Printf("[ERROR] can't parse commentHTML to parse images: %q, error: %v", commentHTML, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
doc.Find("img").Each(func(i int, sl *goquery.Selection) {
|
|
||||||
if im, ok := sl.Attr("src"); ok {
|
|
||||||
if strings.Contains(im, s.ImageAPI) {
|
|
||||||
elems := strings.Split(im, "/")
|
|
||||||
if len(elems) >= 2 {
|
|
||||||
id := elems[len(elems)-2] + "/" + elems[len(elems)-1]
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Contains(im, s.ProxyAPI) {
|
|
||||||
proxiedURL, err := url.Parse(im)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
imgURL, err := base64.URLEncoding.DecodeString(proxiedURL.Query().Get("src"))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
imgID, err := CachedImgID(string(imgURL))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ids = append(ids, imgID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return ids
|
// ExtractNonProxiedPictures gets list of non-proxied images from the doc html and convert from urls to ids, i.e. user/pic.png
|
||||||
|
// This method is used in image check on post preview and load, as proxied images have lazy loading
|
||||||
|
// and wouldn't be present on disk but still valid as they will be loaded the first time someone requests them.
|
||||||
|
func (s *Service) ExtractNonProxiedPictures(commentHTML string) (ids []string) {
|
||||||
|
return s.extractImageIDs(commentHTML, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup runs periodic cleanup with 1.5*ServiceParams.EditDuration. Blocking loop, should be called inside of goroutine by consumer
|
// Cleanup runs periodic cleanup with 1.5*ServiceParams.EditDuration. Blocking loop, should be called inside of goroutine by consumer
|
||||||
@@ -264,6 +237,43 @@ func (s *Service) ImgContentType(img []byte) string {
|
|||||||
return contentType
|
return contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns list of image IDs from the comment html, including proxied images if includeProxied is true
|
||||||
|
func (s *Service) extractImageIDs(commentHTML string, includeProxied bool) (ids []string) {
|
||||||
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(commentHTML))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] can't parse commentHTML to parse images: %q, error: %v", commentHTML, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
doc.Find("img").Each(func(i int, sl *goquery.Selection) {
|
||||||
|
if im, ok := sl.Attr("src"); ok {
|
||||||
|
if strings.Contains(im, s.ImageAPI) {
|
||||||
|
elems := strings.Split(im, "/")
|
||||||
|
if len(elems) >= 2 {
|
||||||
|
id := elems[len(elems)-2] + "/" + elems[len(elems)-1]
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if includeProxied && strings.Contains(im, s.ProxyAPI) {
|
||||||
|
proxiedURL, err := url.Parse(im)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imgURL, err := base64.URLEncoding.DecodeString(proxiedURL.Query().Get("src"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imgID, err := CachedImgID(string(imgURL))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids = append(ids, imgID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
// prepareImage calls readAndValidateImage and resize on provided image.
|
// prepareImage calls readAndValidateImage and resize on provided image.
|
||||||
func (s *Service) prepareImage(r io.Reader) ([]byte, error) {
|
func (s *Service) prepareImage(r io.Reader) ([]byte, error) {
|
||||||
data, err := readAndValidateImage(r, s.MaxSize)
|
data, err := readAndValidateImage(r, s.MaxSize)
|
||||||
@@ -367,7 +377,7 @@ func Sha1Str(s string) string {
|
|||||||
// ID would look like: "cached_images/<sha1-of-image-url-hostname>-<sha1-of-image-entire-url>"
|
// ID would look like: "cached_images/<sha1-of-image-url-hostname>-<sha1-of-image-entire-url>"
|
||||||
// <sha1-of-image-url-hostname> - would allow us to identify all images from particular site if ever needed
|
// <sha1-of-image-url-hostname> - would allow us to identify all images from particular site if ever needed
|
||||||
// <sha1-of-image-entire-url> - would allow us to avoid storing duplicates of the same image
|
// <sha1-of-image-entire-url> - would allow us to avoid storing duplicates of the same image
|
||||||
// (as accurate as deduplication based on potentially mutable url can be)
|
// (as accurate as deduplication based on potentially mutable url can be)
|
||||||
func CachedImgID(imgURL string) (string, error) {
|
func CachedImgID(imgURL string) (string, error) {
|
||||||
parsedURL, err := url.Parse(imgURL)
|
parsedURL, err := url.Parse(imgURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,115 +1,301 @@
|
|||||||
// Code generated by mockery v1.1.2. DO NOT EDIT.
|
// Code generated by moq; DO NOT EDIT.
|
||||||
|
// github.com/matryer/moq
|
||||||
|
|
||||||
package image
|
package image
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
|
"sync"
|
||||||
time "time"
|
time "time"
|
||||||
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockStore is an autogenerated mock type for the Store type
|
// Ensure, that StoreMock does implement Store.
|
||||||
type MockStore struct {
|
// If this is not the case, regenerate this file with moq.
|
||||||
mock.Mock
|
var _ Store = &StoreMock{}
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup provides a mock function with given fields: ctx, ttl
|
// StoreMock is a mock implementation of Store.
|
||||||
func (_m *MockStore) Cleanup(ctx context.Context, ttl time.Duration) error {
|
//
|
||||||
ret := _m.Called(ctx, ttl)
|
// func TestSomethingThatUsesStore(t *testing.T) {
|
||||||
|
//
|
||||||
|
// // make and configure a mocked Store
|
||||||
|
// mockedStore := &StoreMock{
|
||||||
|
// CleanupFunc: func(ctx context.Context, ttl time.Duration) error {
|
||||||
|
// panic("mock out the Cleanup method")
|
||||||
|
// },
|
||||||
|
// CommitFunc: func(id string) error {
|
||||||
|
// panic("mock out the Commit method")
|
||||||
|
// },
|
||||||
|
// InfoFunc: func() (StoreInfo, error) {
|
||||||
|
// panic("mock out the Info method")
|
||||||
|
// },
|
||||||
|
// LoadFunc: func(id string) ([]byte, error) {
|
||||||
|
// panic("mock out the Load method")
|
||||||
|
// },
|
||||||
|
// ResetCleanupTimerFunc: func(id string) error {
|
||||||
|
// panic("mock out the ResetCleanupTimer method")
|
||||||
|
// },
|
||||||
|
// SaveFunc: func(id string, img []byte) error {
|
||||||
|
// panic("mock out the Save method")
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // use mockedStore in code that requires Store
|
||||||
|
// // and then make assertions.
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
type StoreMock struct {
|
||||||
|
// CleanupFunc mocks the Cleanup method.
|
||||||
|
CleanupFunc func(ctx context.Context, ttl time.Duration) error
|
||||||
|
|
||||||
var r0 error
|
// CommitFunc mocks the Commit method.
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, time.Duration) error); ok {
|
CommitFunc func(id string) error
|
||||||
r0 = rf(ctx, ttl)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
// InfoFunc mocks the Info method.
|
||||||
}
|
InfoFunc func() (StoreInfo, error)
|
||||||
|
|
||||||
// Commit provides a mock function with given fields: id
|
// LoadFunc mocks the Load method.
|
||||||
func (_m *MockStore) Commit(id string) error {
|
LoadFunc func(id string) ([]byte, error)
|
||||||
ret := _m.Called(id)
|
|
||||||
|
|
||||||
var r0 error
|
// ResetCleanupTimerFunc mocks the ResetCleanupTimer method.
|
||||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
ResetCleanupTimerFunc func(id string) error
|
||||||
r0 = rf(id)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
// SaveFunc mocks the Save method.
|
||||||
}
|
SaveFunc func(id string, img []byte) error
|
||||||
|
|
||||||
// Info provides a mock function with given fields:
|
// calls tracks calls to the methods.
|
||||||
func (_m *MockStore) Info() (StoreInfo, error) {
|
calls struct {
|
||||||
ret := _m.Called()
|
// Cleanup holds details about calls to the Cleanup method.
|
||||||
|
Cleanup []struct {
|
||||||
var r0 StoreInfo
|
// Ctx is the ctx argument value.
|
||||||
if rf, ok := ret.Get(0).(func() StoreInfo); ok {
|
Ctx context.Context
|
||||||
r0 = rf()
|
// TTL is the ttl argument value.
|
||||||
} else {
|
TTL time.Duration
|
||||||
r0 = ret.Get(0).(StoreInfo)
|
}
|
||||||
}
|
// Commit holds details about calls to the Commit method.
|
||||||
|
Commit []struct {
|
||||||
var r1 error
|
// ID is the id argument value.
|
||||||
if rf, ok := ret.Get(1).(func() error); ok {
|
ID string
|
||||||
r1 = rf()
|
}
|
||||||
} else {
|
// Info holds details about calls to the Info method.
|
||||||
r1 = ret.Error(1)
|
Info []struct {
|
||||||
}
|
}
|
||||||
|
// Load holds details about calls to the Load method.
|
||||||
return r0, r1
|
Load []struct {
|
||||||
}
|
// ID is the id argument value.
|
||||||
|
ID string
|
||||||
// Load provides a mock function with given fields: id
|
}
|
||||||
func (_m *MockStore) Load(id string) ([]byte, error) {
|
// ResetCleanupTimer holds details about calls to the ResetCleanupTimer method.
|
||||||
ret := _m.Called(id)
|
ResetCleanupTimer []struct {
|
||||||
|
// ID is the id argument value.
|
||||||
var r0 []byte
|
ID string
|
||||||
if rf, ok := ret.Get(0).(func(string) []byte); ok {
|
}
|
||||||
r0 = rf(id)
|
// Save holds details about calls to the Save method.
|
||||||
} else {
|
Save []struct {
|
||||||
if ret.Get(0) != nil {
|
// ID is the id argument value.
|
||||||
r0 = ret.Get(0).([]byte)
|
ID string
|
||||||
|
// Img is the img argument value.
|
||||||
|
Img []byte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lockCleanup sync.RWMutex
|
||||||
var r1 error
|
lockCommit sync.RWMutex
|
||||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
lockInfo sync.RWMutex
|
||||||
r1 = rf(id)
|
lockLoad sync.RWMutex
|
||||||
} else {
|
lockResetCleanupTimer sync.RWMutex
|
||||||
r1 = ret.Error(1)
|
lockSave sync.RWMutex
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetCleanupTimer provides a mock function with given fields: id
|
// Cleanup calls CleanupFunc.
|
||||||
func (_m *MockStore) ResetCleanupTimer(id string) error {
|
func (mock *StoreMock) Cleanup(ctx context.Context, ttl time.Duration) error {
|
||||||
ret := _m.Called(id)
|
if mock.CleanupFunc == nil {
|
||||||
|
panic("StoreMock.CleanupFunc: method is nil but Store.Cleanup was just called")
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
|
||||||
r0 = rf(id)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
}
|
||||||
|
callInfo := struct {
|
||||||
return r0
|
Ctx context.Context
|
||||||
|
TTL time.Duration
|
||||||
|
}{
|
||||||
|
Ctx: ctx,
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
mock.lockCleanup.Lock()
|
||||||
|
mock.calls.Cleanup = append(mock.calls.Cleanup, callInfo)
|
||||||
|
mock.lockCleanup.Unlock()
|
||||||
|
return mock.CleanupFunc(ctx, ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save provides a mock function with given fields: id, img
|
// CleanupCalls gets all the calls that were made to Cleanup.
|
||||||
func (_m *MockStore) Save(id string, img []byte) error {
|
// Check the length with:
|
||||||
ret := _m.Called(id, img)
|
//
|
||||||
|
// len(mockedStore.CleanupCalls())
|
||||||
var r0 error
|
func (mock *StoreMock) CleanupCalls() []struct {
|
||||||
if rf, ok := ret.Get(0).(func(string, []byte) error); ok {
|
Ctx context.Context
|
||||||
r0 = rf(id, img)
|
TTL time.Duration
|
||||||
} else {
|
} {
|
||||||
r0 = ret.Error(0)
|
var calls []struct {
|
||||||
|
Ctx context.Context
|
||||||
|
TTL time.Duration
|
||||||
}
|
}
|
||||||
|
mock.lockCleanup.RLock()
|
||||||
|
calls = mock.calls.Cleanup
|
||||||
|
mock.lockCleanup.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
return r0
|
// Commit calls CommitFunc.
|
||||||
|
func (mock *StoreMock) Commit(id string) error {
|
||||||
|
if mock.CommitFunc == nil {
|
||||||
|
panic("StoreMock.CommitFunc: method is nil but Store.Commit was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
ID string
|
||||||
|
}{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
mock.lockCommit.Lock()
|
||||||
|
mock.calls.Commit = append(mock.calls.Commit, callInfo)
|
||||||
|
mock.lockCommit.Unlock()
|
||||||
|
return mock.CommitFunc(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommitCalls gets all the calls that were made to Commit.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedStore.CommitCalls())
|
||||||
|
func (mock *StoreMock) CommitCalls() []struct {
|
||||||
|
ID string
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
mock.lockCommit.RLock()
|
||||||
|
calls = mock.calls.Commit
|
||||||
|
mock.lockCommit.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info calls InfoFunc.
|
||||||
|
func (mock *StoreMock) Info() (StoreInfo, error) {
|
||||||
|
if mock.InfoFunc == nil {
|
||||||
|
panic("StoreMock.InfoFunc: method is nil but Store.Info was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
}{}
|
||||||
|
mock.lockInfo.Lock()
|
||||||
|
mock.calls.Info = append(mock.calls.Info, callInfo)
|
||||||
|
mock.lockInfo.Unlock()
|
||||||
|
return mock.InfoFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoCalls gets all the calls that were made to Info.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedStore.InfoCalls())
|
||||||
|
func (mock *StoreMock) InfoCalls() []struct {
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
}
|
||||||
|
mock.lockInfo.RLock()
|
||||||
|
calls = mock.calls.Info
|
||||||
|
mock.lockInfo.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load calls LoadFunc.
|
||||||
|
func (mock *StoreMock) Load(id string) ([]byte, error) {
|
||||||
|
if mock.LoadFunc == nil {
|
||||||
|
panic("StoreMock.LoadFunc: method is nil but Store.Load was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
ID string
|
||||||
|
}{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
mock.lockLoad.Lock()
|
||||||
|
mock.calls.Load = append(mock.calls.Load, callInfo)
|
||||||
|
mock.lockLoad.Unlock()
|
||||||
|
return mock.LoadFunc(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCalls gets all the calls that were made to Load.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedStore.LoadCalls())
|
||||||
|
func (mock *StoreMock) LoadCalls() []struct {
|
||||||
|
ID string
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
mock.lockLoad.RLock()
|
||||||
|
calls = mock.calls.Load
|
||||||
|
mock.lockLoad.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetCleanupTimer calls ResetCleanupTimerFunc.
|
||||||
|
func (mock *StoreMock) ResetCleanupTimer(id string) error {
|
||||||
|
if mock.ResetCleanupTimerFunc == nil {
|
||||||
|
panic("StoreMock.ResetCleanupTimerFunc: method is nil but Store.ResetCleanupTimer was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
ID string
|
||||||
|
}{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
mock.lockResetCleanupTimer.Lock()
|
||||||
|
mock.calls.ResetCleanupTimer = append(mock.calls.ResetCleanupTimer, callInfo)
|
||||||
|
mock.lockResetCleanupTimer.Unlock()
|
||||||
|
return mock.ResetCleanupTimerFunc(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetCleanupTimerCalls gets all the calls that were made to ResetCleanupTimer.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedStore.ResetCleanupTimerCalls())
|
||||||
|
func (mock *StoreMock) ResetCleanupTimerCalls() []struct {
|
||||||
|
ID string
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
mock.lockResetCleanupTimer.RLock()
|
||||||
|
calls = mock.calls.ResetCleanupTimer
|
||||||
|
mock.lockResetCleanupTimer.RUnlock()
|
||||||
|
return calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save calls SaveFunc.
|
||||||
|
func (mock *StoreMock) Save(id string, img []byte) error {
|
||||||
|
if mock.SaveFunc == nil {
|
||||||
|
panic("StoreMock.SaveFunc: method is nil but Store.Save was just called")
|
||||||
|
}
|
||||||
|
callInfo := struct {
|
||||||
|
ID string
|
||||||
|
Img []byte
|
||||||
|
}{
|
||||||
|
ID: id,
|
||||||
|
Img: img,
|
||||||
|
}
|
||||||
|
mock.lockSave.Lock()
|
||||||
|
mock.calls.Save = append(mock.calls.Save, callInfo)
|
||||||
|
mock.lockSave.Unlock()
|
||||||
|
return mock.SaveFunc(id, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveCalls gets all the calls that were made to Save.
|
||||||
|
// Check the length with:
|
||||||
|
//
|
||||||
|
// len(mockedStore.SaveCalls())
|
||||||
|
func (mock *StoreMock) SaveCalls() []struct {
|
||||||
|
ID string
|
||||||
|
Img []byte
|
||||||
|
} {
|
||||||
|
var calls []struct {
|
||||||
|
ID string
|
||||||
|
Img []byte
|
||||||
|
}
|
||||||
|
mock.lockSave.RLock()
|
||||||
|
calls = mock.calls.Save
|
||||||
|
mock.lockSave.RUnlock()
|
||||||
|
return calls
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,22 +14,30 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestService_SaveAndLoad(t *testing.T) {
|
func TestService_SaveAndLoad(t *testing.T) {
|
||||||
store := MockStore{}
|
store := StoreMock{
|
||||||
|
SaveFunc: func(id string, img []byte) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
LoadFunc: func(id string) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
svc := NewService(&store, ServiceParams{MaxSize: 1500, MaxWidth: 32, MaxHeight: 32})
|
svc := NewService(&store, ServiceParams{MaxSize: 1500, MaxWidth: 32, MaxHeight: 32})
|
||||||
|
|
||||||
store.On("Save", "test_id", mock.Anything).Return(nil)
|
|
||||||
err := svc.SaveWithID("test_id", gopherPNG())
|
err := svc.SaveWithID("test_id", gopherPNG())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(store.SaveCalls()))
|
||||||
|
assert.Equal(t, "test_id", store.SaveCalls()[0].ID)
|
||||||
|
|
||||||
store.On("Load", "test_id", mock.Anything).Return(nil, nil)
|
|
||||||
img, err := svc.Load("test_id")
|
img, err := svc.Load("test_id")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Nil(t, img)
|
assert.Nil(t, img)
|
||||||
|
assert.Equal(t, 1, len(store.LoadCalls()))
|
||||||
|
assert.Equal(t, "test_id", store.LoadCalls()[0].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_Resize(t *testing.T) {
|
func TestService_Resize(t *testing.T) {
|
||||||
@@ -97,6 +105,7 @@ func TestService_ExtractPictures(t *testing.T) {
|
|||||||
ids = svc.ExtractPictures(html)
|
ids = svc.ExtractPictures(html)
|
||||||
require.Equal(t, 1, len(ids), "one image in")
|
require.Equal(t, 1, len(ids), "one image in")
|
||||||
assert.Equal(t, "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e", ids[0])
|
assert.Equal(t, "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e", ids[0])
|
||||||
|
require.Empty(t, svc.ExtractNonProxiedPictures(html), "no non-proxied images expected to be found")
|
||||||
|
|
||||||
// bad url
|
// bad url
|
||||||
html = `<img src=" https://remark42.radio-t.com/api/v1/img">`
|
html = `<img src=" https://remark42.radio-t.com/api/v1/img">`
|
||||||
@@ -116,72 +125,91 @@ func TestService_ExtractPictures(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestService_Cleanup(t *testing.T) {
|
func TestService_Cleanup(t *testing.T) {
|
||||||
store := MockStore{}
|
store := StoreMock{
|
||||||
store.On("Cleanup", mock.Anything, mock.Anything).Times(10).Return(nil)
|
CleanupFunc: func(ctx context.Context, ttl time.Duration) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond})
|
svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond})
|
||||||
// cancel context after 2.1 cleanup TTLs
|
// cancel context after 2.1 cleanup TTLs
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), svc.EditDuration/100*15*21)
|
ctx, cancel := context.WithTimeout(context.Background(), svc.EditDuration/100*15*21)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
svc.Cleanup(ctx)
|
svc.Cleanup(ctx)
|
||||||
store.AssertNumberOfCalls(t, "Cleanup", 2)
|
assert.Equal(t, 2, len(store.CleanupCalls()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_Submit(t *testing.T) {
|
func TestService_Submit(t *testing.T) {
|
||||||
store := MockStore{}
|
store := StoreMock{
|
||||||
store.On("Commit", mock.Anything, mock.Anything).Times(7).Return(nil)
|
CommitFunc: func(id string) error {
|
||||||
store.On("ResetCleanupTimer", mock.Anything, mock.Anything).Times(7).Return(nil)
|
return nil
|
||||||
|
},
|
||||||
|
ResetCleanupTimerFunc: func(id string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
svc := NewService(&store, ServiceParams{ImageAPI: "/blah/", EditDuration: time.Millisecond * 100})
|
svc := NewService(&store, ServiceParams{ImageAPI: "/blah/", EditDuration: time.Millisecond * 100})
|
||||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||||
store.AssertNumberOfCalls(t, "ResetCleanupTimer", 3)
|
assert.Equal(t, 3, len(store.ResetCleanupTimerCalls()))
|
||||||
err := svc.SubmitAndCommit(func() []string { return []string{"id4", "id5"} })
|
err := svc.Commit(func() []string { return []string{"id4", "id5"} })
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
svc.Submit(func() []string { return []string{"id6", "id7"} })
|
svc.Submit(func() []string { return []string{"id6", "id7"} })
|
||||||
store.AssertNumberOfCalls(t, "ResetCleanupTimer", 5)
|
assert.Equal(t, 5, len(store.ResetCleanupTimerCalls()))
|
||||||
svc.Submit(nil)
|
svc.Submit(nil)
|
||||||
store.AssertNumberOfCalls(t, "Commit", 2)
|
assert.Equal(t, 2, len(store.CommitCalls()))
|
||||||
time.Sleep(time.Millisecond * 175)
|
time.Sleep(time.Millisecond * 175)
|
||||||
store.AssertNumberOfCalls(t, "Commit", 7)
|
assert.Equal(t, 7, len(store.CommitCalls()))
|
||||||
svc.Close(context.TODO())
|
svc.Close(context.TODO())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_Close(t *testing.T) {
|
func TestService_Close(t *testing.T) {
|
||||||
store := MockStore{}
|
store := StoreMock{
|
||||||
store.On("Commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
CommitFunc: func(id string) error {
|
||||||
store.On("ResetCleanupTimer", mock.Anything, mock.Anything).Times(5).Return(nil)
|
return nil
|
||||||
|
},
|
||||||
|
ResetCleanupTimerFunc: func(id string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
svc := Service{store: &store, ServiceParams: ServiceParams{ImageAPI: "/blah/", EditDuration: time.Hour * 24}}
|
svc := Service{store: &store, ServiceParams: ServiceParams{ImageAPI: "/blah/", EditDuration: time.Hour * 24}}
|
||||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||||
svc.Submit(nil)
|
svc.Submit(nil)
|
||||||
store.AssertNumberOfCalls(t, "ResetCleanupTimer", 5)
|
assert.Equal(t, 5, len(store.ResetCleanupTimerCalls()))
|
||||||
svc.Close(context.TODO())
|
svc.Close(context.TODO())
|
||||||
store.AssertNumberOfCalls(t, "Commit", 5)
|
assert.Equal(t, 5, len(store.CommitCalls()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_SubmitDelay(t *testing.T) {
|
func TestService_SubmitDelay(t *testing.T) {
|
||||||
store := MockStore{}
|
store := StoreMock{
|
||||||
store.On("Commit", mock.Anything, mock.Anything).Times(5).Return(nil)
|
CommitFunc: func(id string) error {
|
||||||
store.On("ResetCleanupTimer", mock.Anything, mock.Anything).Times(5).Return(nil)
|
return nil
|
||||||
|
},
|
||||||
|
ResetCleanupTimerFunc: func(id string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond})
|
svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond})
|
||||||
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
svc.Submit(func() []string { return []string{"id1", "id2", "id3"} })
|
||||||
time.Sleep(150 * time.Millisecond) // let first batch to pass TTL
|
time.Sleep(150 * time.Millisecond) // let first batch to pass TTL
|
||||||
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
svc.Submit(func() []string { return []string{"id4", "id5"} })
|
||||||
svc.Submit(nil)
|
svc.Submit(nil)
|
||||||
store.AssertNumberOfCalls(t, "ResetCleanupTimer", 5)
|
assert.Equal(t, 5, len(store.ResetCleanupTimerCalls()))
|
||||||
store.AssertNumberOfCalls(t, "Commit", 3)
|
assert.Equal(t, 3, len(store.CommitCalls()))
|
||||||
svc.Close(context.TODO())
|
svc.Close(context.TODO())
|
||||||
store.AssertNumberOfCalls(t, "Commit", 5)
|
assert.Equal(t, 5, len(store.CommitCalls()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_Info(t *testing.T) {
|
func TestService_Info(t *testing.T) {
|
||||||
store := MockStore{}
|
store := StoreMock{InfoFunc: func() (StoreInfo, error) {
|
||||||
store.On("Info", mock.Anything, mock.Anything).Once().Return(StoreInfo{}, nil)
|
return StoreInfo{}, nil
|
||||||
|
}}
|
||||||
|
|
||||||
svc := Service{store: &store, ServiceParams: ServiceParams{}}
|
svc := Service{store: &store, ServiceParams: ServiceParams{}}
|
||||||
info, err := svc.Info()
|
info, err := svc.Info()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, info.FirstStagingImageTS.IsZero())
|
assert.True(t, info.FirstStagingImageTS.IsZero())
|
||||||
store.AssertNumberOfCalls(t, "Info", 1)
|
assert.Equal(t, 1, len(store.InfoCalls()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_resize(t *testing.T) {
|
func TestService_resize(t *testing.T) {
|
||||||
@@ -259,3 +287,11 @@ func TestCachedImgID(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "cached_images/"+Sha1Str("example.org")+"-"+Sha1Str(imgURL), img)
|
assert.Equal(t, "cached_images/"+Sha1Str("example.org")+"-"+Sha1Str(imgURL), img)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestService_DoubleClose(*testing.T) {
|
||||||
|
store := StoreMock{}
|
||||||
|
svc := NewService(&store, ServiceParams{EditDuration: 20 * time.Millisecond})
|
||||||
|
svc.Close(context.TODO())
|
||||||
|
// second call should not result in panic
|
||||||
|
svc.Close(context.TODO())
|
||||||
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -14,6 +14,7 @@ import (
|
|||||||
log "github.com/go-pkgz/lgr"
|
log "github.com/go-pkgz/lgr"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
|
bf "github.com/russross/blackfriday/v2"
|
||||||
|
|
||||||
"github.com/umputun/remark42/backend/app/store"
|
"github.com/umputun/remark42/backend/app/store"
|
||||||
"github.com/umputun/remark42/backend/app/store/admin"
|
"github.com/umputun/remark42/backend/app/store/admin"
|
||||||
@@ -278,7 +279,7 @@ func (s *DataStore) submitImages(comment store.Comment) {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
if comment.Imported {
|
if comment.Imported {
|
||||||
err = s.ImageService.SubmitAndCommit(idsFn)
|
err = s.ImageService.Commit(idsFn)
|
||||||
} else {
|
} else {
|
||||||
s.ImageService.Submit(idsFn)
|
s.ImageService.Submit(idsFn)
|
||||||
}
|
}
|
||||||
@@ -496,6 +497,12 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi
|
|||||||
if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvDelete); e != nil {
|
if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvDelete); e != nil {
|
||||||
log.Printf("[WARN] failed to send delete event, %s", e)
|
log.Printf("[WARN] failed to send delete event, %s", e)
|
||||||
}
|
}
|
||||||
|
// clean up the comment and its parent from cache, so that
|
||||||
|
// after cleaning up the child, parent won't be stuck non-deletable till cache expires
|
||||||
|
if s.repliesCache.LoadingCache != nil {
|
||||||
|
s.repliesCache.Delete(commentID)
|
||||||
|
s.repliesCache.Delete(comment.ParentID)
|
||||||
|
}
|
||||||
comment.Deleted = true
|
comment.Deleted = true
|
||||||
delReq := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: store.SoftDelete}
|
delReq := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: store.SoftDelete}
|
||||||
return comment, s.Engine.Delete(delReq)
|
return comment, s.Engine.Delete(delReq)
|
||||||
@@ -621,7 +628,9 @@ func (s *DataStore) Counts(siteID string, postIDs []string) ([]store.PostInfo, e
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateComment checks if comment size below max and user fields set
|
// ValidateComment checks if comment size below max and user fields set.
|
||||||
|
// It also validates the absence of relative links as they are almost never the intention of the commenter,
|
||||||
|
// usually added by mistakes and only create confusion.
|
||||||
func (s *DataStore) ValidateComment(c *store.Comment) error {
|
func (s *DataStore) ValidateComment(c *store.Comment) error {
|
||||||
maxSize := s.MaxCommentSize
|
maxSize := s.MaxCommentSize
|
||||||
if s.MaxCommentSize <= 0 {
|
if s.MaxCommentSize <= 0 {
|
||||||
@@ -636,7 +645,21 @@ func (s *DataStore) ValidateComment(c *store.Comment) error {
|
|||||||
if c.User.ID == "" || c.User.Name == "" {
|
if c.User.ID == "" || c.User.Name == "" {
|
||||||
return fmt.Errorf("empty user info")
|
return fmt.Errorf("empty user info")
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
mdExt, rend := store.GetMdExtensionsAndRenderer()
|
||||||
|
parser := bf.New(bf.WithRenderer(rend), bf.WithExtensions(bf.CommonExtensions), bf.WithExtensions(mdExt))
|
||||||
|
var wrongLinkError error
|
||||||
|
parser.Parse([]byte(c.Orig)).Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
|
||||||
|
if len(node.LinkData.Destination) != 0 &&
|
||||||
|
!(strings.HasPrefix(string(node.LinkData.Destination), "http://") ||
|
||||||
|
strings.HasPrefix(string(node.LinkData.Destination), "https://") ||
|
||||||
|
strings.HasPrefix(string(node.LinkData.Destination), "mailto:")) {
|
||||||
|
wrongLinkError = fmt.Errorf("links should start with mailto:, http:// or https://")
|
||||||
|
return bf.Terminate
|
||||||
|
}
|
||||||
|
return bf.GoToNext
|
||||||
|
})
|
||||||
|
return wrongLinkError
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAdmin checks if usesID in the list of admins
|
// IsAdmin checks if usesID in the list of admins
|
||||||
@@ -739,6 +762,17 @@ func (s *DataStore) Delete(locator store.Locator, commentID string, mode store.D
|
|||||||
if e := s.AdminStore.OnEvent(locator.SiteID, admin.EvDelete); e != nil {
|
if e := s.AdminStore.OnEvent(locator.SiteID, admin.EvDelete); e != nil {
|
||||||
log.Printf("[WARN] failed to send delete event, %s", e)
|
log.Printf("[WARN] failed to send delete event, %s", e)
|
||||||
}
|
}
|
||||||
|
// get comment to learn its parent ID
|
||||||
|
comment, err := s.Engine.Get(engine.GetRequest{Locator: locator, CommentID: commentID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// clean up the comment and its parent from cache, so that
|
||||||
|
// after cleaning up the child, parent won't be stuck non-deletable till cache expires
|
||||||
|
if s.repliesCache.LoadingCache != nil {
|
||||||
|
s.repliesCache.Delete(commentID)
|
||||||
|
s.repliesCache.Delete(comment.ParentID)
|
||||||
|
}
|
||||||
req := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: mode}
|
req := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: mode}
|
||||||
return s.Engine.Delete(req)
|
return s.Engine.Delete(req)
|
||||||
}
|
}
|
||||||
@@ -981,9 +1015,9 @@ func (s *DataStore) prepVotes(c store.Comment, user store.User) store.Comment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get secret for given siteID
|
// get secret for given siteID
|
||||||
// Note: secret shared across sites, but some sites can be disabled.
|
// Note: siteID ignored for the default admin.Static store
|
||||||
func (s *DataStore) getSecret(siteID string) (secret string, err error) {
|
func (s *DataStore) getSecret(siteID string) (secret string, err error) {
|
||||||
if secret, err = s.AdminStore.Key("any"); err != nil {
|
if secret, err = s.AdminStore.Key(siteID); err != nil {
|
||||||
return "", fmt.Errorf("can't get secret for site %s: %w", siteID, err)
|
return "", fmt.Errorf("can't get secret for site %s: %w", siteID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -414,7 +413,6 @@ func TestService_VoteAggressive(t *testing.T) {
|
|||||||
assert.Equal(t, 0, len(res[0].VotedIPs), "vote ips hidden")
|
assert.Equal(t, 0, len(res[0].VotedIPs), "vote ips hidden")
|
||||||
|
|
||||||
// random +1/-1 result should be [0..2]
|
// random +1/-1 result should be [0..2]
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -781,22 +779,25 @@ func TestService_ValidateComment(t *testing.T) {
|
|||||||
|
|
||||||
tbl := []struct {
|
tbl := []struct {
|
||||||
inp store.Comment
|
inp store.Comment
|
||||||
err error
|
err string
|
||||||
}{
|
}{
|
||||||
{inp: store.Comment{}, err: fmt.Errorf("empty comment text")},
|
{inp: store.Comment{}, err: "empty comment text"},
|
||||||
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid", Name: "name"}}, err: nil},
|
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid", Name: "name"}}, err: ""},
|
||||||
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid"}}, err: fmt.Errorf("empty user info")},
|
{inp: store.Comment{Orig: "something blah", User: store.User{ID: "myid"}}, err: "empty user info"},
|
||||||
{inp: store.Comment{Orig: longText, User: store.User{ID: "myid", Name: "name"}}, err: fmt.Errorf("comment text exceeded max allowed size 2000 (4000)")},
|
{inp: store.Comment{Orig: longText, User: store.User{ID: "myid", Name: "name"}}, err: "comment text exceeded max allowed size 2000 (4000)"},
|
||||||
|
{inp: store.Comment{Orig: "here is a link with relative URL: [google.com](url)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
|
||||||
|
{inp: store.Comment{Orig: "here is a link with relative URL: [google.com](url)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
|
||||||
|
{inp: store.Comment{Orig: "multiple links, one is bad: [test](http://test) [test2](bad_url) [test3](https://test3)", User: store.User{ID: "myid", Name: "name"}}, err: "links should start with mailto:, http:// or https://"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for n, tt := range tbl {
|
for n, tt := range tbl {
|
||||||
err := b.ValidateComment(&tt.inp)
|
err := b.ValidateComment(&tt.inp)
|
||||||
if tt.err == nil {
|
if tt.err == "" {
|
||||||
assert.NoError(t, err, "check #%d", n)
|
assert.NoError(t, err, "check #%d", n)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.EqualError(t, tt.err, err.Error(), "check #%d", n)
|
assert.EqualError(t, err, tt.err, "check #%d", n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -972,7 +973,7 @@ func TestService_HasReplies(t *testing.T) {
|
|||||||
// two comments for https://radio-t.com, no reply
|
// two comments for https://radio-t.com, no reply
|
||||||
eng, teardown := prepStoreEngine(t)
|
eng, teardown := prepStoreEngine(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
b := DataStore{Engine: eng, EditDuration: 100 * time.Millisecond,
|
b := DataStore{Engine: eng,
|
||||||
AdminStore: admin.NewStaticStore("secret 123", []string{"radio-t"}, []string{"user2"}, "user@email.com")}
|
AdminStore: admin.NewStaticStore("secret 123", []string{"radio-t"}, []string{"user2"}, "user@email.com")}
|
||||||
defer b.Close()
|
defer b.Close()
|
||||||
|
|
||||||
@@ -983,11 +984,10 @@ func TestService_HasReplies(t *testing.T) {
|
|||||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||||
User: store.User{ID: "user1", Name: "user name"},
|
User: store.User{ID: "user1", Name: "user name"},
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.False(t, b.HasReplies(comment))
|
assert.False(t, b.HasReplies(comment))
|
||||||
|
|
||||||
reply := store.Comment{
|
reply := store.Comment{
|
||||||
ID: "123456",
|
ID: "c-1",
|
||||||
ParentID: "id-1",
|
ParentID: "id-1",
|
||||||
Text: "some text",
|
Text: "some text",
|
||||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||||
@@ -996,7 +996,45 @@ func TestService_HasReplies(t *testing.T) {
|
|||||||
}
|
}
|
||||||
_, err := b.Create(reply)
|
_, err := b.Create(reply)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
_, found := b.repliesCache.Peek(comment.ID)
|
||||||
|
assert.False(t, found, "not yet checked")
|
||||||
assert.True(t, b.HasReplies(comment))
|
assert.True(t, b.HasReplies(comment))
|
||||||
|
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||||
|
assert.True(t, found, "checked and has replies")
|
||||||
|
|
||||||
|
// deletion of the parent comment shouldn't work as the comment has replies
|
||||||
|
_, err = b.EditComment(reply.Locator, comment.ID, EditRequest{Orig: comment.ID, Delete: true, Summary: "user deletes the comment"})
|
||||||
|
assert.EqualError(t, err, "parent comment with reply can't be edited, "+comment.ID)
|
||||||
|
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||||
|
assert.True(t, found, "checked and has replies")
|
||||||
|
|
||||||
|
// should not report replies after deletion of the child
|
||||||
|
err = b.Delete(reply.Locator, reply.ID, store.HardDelete)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||||
|
assert.False(t, found, "cleaned up from cache by Delete call")
|
||||||
|
assert.False(t, b.HasReplies(comment))
|
||||||
|
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||||
|
assert.False(t, found, "checked and has no replies")
|
||||||
|
|
||||||
|
// recreate reply with the new ID
|
||||||
|
reply.ID = "c-2"
|
||||||
|
_, err = b.Create(reply)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||||
|
assert.False(t, found, "not yet checked")
|
||||||
|
assert.True(t, b.HasReplies(comment))
|
||||||
|
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||||
|
assert.True(t, found, "checked and has replies again")
|
||||||
|
|
||||||
|
// should not report replies after deletion of the child using Edit mechanism
|
||||||
|
_, err = b.EditComment(reply.Locator, reply.ID, EditRequest{Orig: reply.ID, Delete: true, Summary: "user deletes the comment"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||||
|
assert.False(t, found, "cleaned up from cache by EditComment call")
|
||||||
|
assert.False(t, b.HasReplies(comment))
|
||||||
|
_, found = b.repliesCache.Peek(reply.ParentID)
|
||||||
|
assert.False(t, found, "checked and has no replies")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_UserReplies(t *testing.T) {
|
func TestService_UserReplies(t *testing.T) {
|
||||||
@@ -1359,11 +1397,10 @@ func TestService_DeleteAll(t *testing.T) {
|
|||||||
func TestService_submitImages(t *testing.T) {
|
func TestService_submitImages(t *testing.T) {
|
||||||
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
|
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
|
||||||
|
|
||||||
mockStore := image.MockStore{}
|
mockStore := image.StoreMock{
|
||||||
mockStore.On("Commit", "dev/pic1.png").Once().Return(nil)
|
CommitFunc: func(id string) error { return nil },
|
||||||
mockStore.On("Commit", "dev/pic2.png").Once().Return(nil)
|
ResetCleanupTimerFunc: func(id string) error { return nil },
|
||||||
mockStore.On("ResetCleanupTimer", "dev/pic1.png").Once().Return(nil)
|
}
|
||||||
mockStore.On("ResetCleanupTimer", "dev/pic2.png").Once().Return(nil)
|
|
||||||
imgSvc := image.NewService(&mockStore,
|
imgSvc := image.NewService(&mockStore,
|
||||||
image.ServiceParams{
|
image.ServiceParams{
|
||||||
EditDuration: 50 * time.Millisecond,
|
EditDuration: 50 * time.Millisecond,
|
||||||
@@ -1389,13 +1426,25 @@ func TestService_submitImages(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
b.submitImages(c)
|
b.submitImages(c)
|
||||||
mockStore.AssertNumberOfCalls(t, "ResetCleanupTimer", 2)
|
assert.Equal(t, 2, len(mockStore.ResetCleanupTimerCalls()))
|
||||||
|
assert.Equal(t, "dev/pic1.png", mockStore.ResetCleanupTimerCalls()[0].ID)
|
||||||
|
assert.Equal(t, "dev/pic2.png", mockStore.ResetCleanupTimerCalls()[1].ID)
|
||||||
time.Sleep(b.EditDuration + 100*time.Millisecond)
|
time.Sleep(b.EditDuration + 100*time.Millisecond)
|
||||||
mockStore.AssertNumberOfCalls(t, "Commit", 2)
|
assert.Equal(t, 2, len(mockStore.CommitCalls()))
|
||||||
|
assert.Equal(t, "dev/pic1.png", mockStore.CommitCalls()[0].ID)
|
||||||
|
assert.Equal(t, "dev/pic2.png", mockStore.CommitCalls()[1].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_ResubmitStagingImages(t *testing.T) {
|
func TestService_ResubmitStagingImages(t *testing.T) {
|
||||||
mockStore := image.MockStore{}
|
mockStore := image.StoreMock{
|
||||||
|
InfoFunc: func() (image.StoreInfo, error) {
|
||||||
|
return image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil
|
||||||
|
},
|
||||||
|
CommitFunc: func(id string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ResetCleanupTimerFunc: func(id string) error { return nil },
|
||||||
|
}
|
||||||
imgSvc := image.NewService(&mockStore,
|
imgSvc := image.NewService(&mockStore,
|
||||||
image.ServiceParams{
|
image.ServiceParams{
|
||||||
EditDuration: 10 * time.Millisecond,
|
EditDuration: 10 * time.Millisecond,
|
||||||
@@ -1408,10 +1457,6 @@ func TestService_ResubmitStagingImages(t *testing.T) {
|
|||||||
defer teardown()
|
defer teardown()
|
||||||
b := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvc}
|
b := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvc}
|
||||||
|
|
||||||
mockStore.On("ResetCleanupTimer", "dev_user/bqf122eq9r8ad657n3ng").Once().Return(nil)
|
|
||||||
mockStore.On("ResetCleanupTimer", "dev_user/bqf321eq9r8ad657n3ng").Once().Return(nil)
|
|
||||||
mockStore.On("ResetCleanupTimer", "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e").Once().Return(nil)
|
|
||||||
|
|
||||||
// create comment with three images without preparing it properly
|
// create comment with three images without preparing it properly
|
||||||
comment := store.Comment{
|
comment := store.Comment{
|
||||||
ID: "id-0",
|
ID: "id-0",
|
||||||
@@ -1427,21 +1472,19 @@ func TestService_ResubmitStagingImages(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// resubmit single comment with three images, of which two are in staging storage
|
// resubmit single comment with three images, of which two are in staging storage
|
||||||
mockStore.On("Info").Once().Return(image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil)
|
|
||||||
err = b.ResubmitStagingImages([]string{"radio-t"})
|
err = b.ResubmitStagingImages([]string{"radio-t"})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// wait for Submit goroutine to commit image
|
// wait for Submit goroutine to commit image
|
||||||
mockStore.On("Commit", "dev_user/bqf122eq9r8ad657n3ng").Once().Return(nil)
|
|
||||||
mockStore.On("Commit", "dev_user/bqf321eq9r8ad657n3ng").Once().Return(nil)
|
|
||||||
mockStore.On("Commit", "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e").Once().Return(nil)
|
|
||||||
time.Sleep(b.EditDuration + time.Millisecond*100)
|
time.Sleep(b.EditDuration + time.Millisecond*100)
|
||||||
|
|
||||||
mockStore.AssertNumberOfCalls(t, "Info", 1)
|
assert.Equal(t, 1, len(mockStore.InfoCalls()))
|
||||||
mockStore.AssertNumberOfCalls(t, "Commit", 3)
|
assert.Equal(t, 3, len(mockStore.CommitCalls()))
|
||||||
|
|
||||||
// empty answer
|
// empty answer
|
||||||
mockStoreEmpty := image.MockStore{}
|
mockStoreEmpty := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) {
|
||||||
|
return image.StoreInfo{FirstStagingImageTS: time.Time{}}, nil
|
||||||
|
}}
|
||||||
imgSvcEmpty := image.NewService(&mockStoreEmpty,
|
imgSvcEmpty := image.NewService(&mockStoreEmpty,
|
||||||
image.ServiceParams{
|
image.ServiceParams{
|
||||||
EditDuration: 10 * time.Millisecond,
|
EditDuration: 10 * time.Millisecond,
|
||||||
@@ -1450,15 +1493,16 @@ func TestService_ResubmitStagingImages(t *testing.T) {
|
|||||||
defer imgSvcEmpty.Close(context.TODO())
|
defer imgSvcEmpty.Close(context.TODO())
|
||||||
bEmpty := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcEmpty}
|
bEmpty := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcEmpty}
|
||||||
|
|
||||||
// resubmit receive empty timestamp and should do nothing
|
// resubmit receive empty timestamp and should do nothing )
|
||||||
mockStoreEmpty.On("Info").Once().Return(image.StoreInfo{FirstStagingImageTS: time.Time{}}, nil)
|
|
||||||
err = bEmpty.ResubmitStagingImages([]string{"radio-t", "non_existent"})
|
err = bEmpty.ResubmitStagingImages([]string{"radio-t", "non_existent"})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
mockStoreEmpty.AssertNumberOfCalls(t, "Info", 1)
|
assert.Equal(t, 1, len(mockStore.InfoCalls()))
|
||||||
|
|
||||||
// error from image storage
|
// error from image storage
|
||||||
mockStoreError := image.MockStore{}
|
mockStoreError := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) {
|
||||||
|
return image.StoreInfo{}, fmt.Errorf("mock_err")
|
||||||
|
}}
|
||||||
imgSvcError := image.NewService(&mockStoreError,
|
imgSvcError := image.NewService(&mockStoreError,
|
||||||
image.ServiceParams{
|
image.ServiceParams{
|
||||||
EditDuration: 10 * time.Millisecond,
|
EditDuration: 10 * time.Millisecond,
|
||||||
@@ -1468,15 +1512,20 @@ func TestService_ResubmitStagingImages(t *testing.T) {
|
|||||||
bError := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcError}
|
bError := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcError}
|
||||||
|
|
||||||
// resubmit will receive error from image storage and should return it
|
// resubmit will receive error from image storage and should return it
|
||||||
mockStoreError.On("Info").Once().Return(image.StoreInfo{}, fmt.Errorf("mock_err"))
|
|
||||||
err = bError.ResubmitStagingImages([]string{"radio-t"})
|
err = bError.ResubmitStagingImages([]string{"radio-t"})
|
||||||
assert.EqualError(t, err, "mock_err")
|
assert.EqualError(t, err, "mock_err")
|
||||||
|
|
||||||
mockStoreError.AssertNumberOfCalls(t, "Info", 1)
|
assert.Equal(t, 1, len(mockStore.InfoCalls()))
|
||||||
|
assert.Equal(t, 3, len(mockStore.ResetCleanupTimerCalls()))
|
||||||
|
assert.Equal(t, "dev_user/bqf122eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[0].ID)
|
||||||
|
assert.Equal(t, "dev_user/bqf321eq9r8ad657n3ng", mockStore.ResetCleanupTimerCalls()[1].ID)
|
||||||
|
assert.Equal(t, "cached_images/12318fbd4c55e9d177b8b5ae197bc89c5afd8e07-a41fcb00643f28d700504256ec81cbf2e1aac53e", mockStore.ResetCleanupTimerCalls()[2].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_ResubmitStagingImages_EngineError(t *testing.T) {
|
func TestService_ResubmitStagingImages_EngineError(t *testing.T) {
|
||||||
mockStore := image.MockStore{}
|
mockStore := image.StoreMock{InfoFunc: func() (image.StoreInfo, error) {
|
||||||
|
return image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil
|
||||||
|
}}
|
||||||
imgSvc := image.NewService(&mockStore,
|
imgSvc := image.NewService(&mockStore,
|
||||||
image.ServiceParams{
|
image.ServiceParams{
|
||||||
EditDuration: 10 * time.Millisecond,
|
EditDuration: 10 * time.Millisecond,
|
||||||
@@ -1484,26 +1533,37 @@ func TestService_ResubmitStagingImages_EngineError(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer imgSvc.Close(context.TODO())
|
defer imgSvc.Close(context.TODO())
|
||||||
|
|
||||||
engineMock := engine.MockInterface{}
|
first := true
|
||||||
|
engineMock := engine.InterfaceMock{
|
||||||
|
FindFunc: func(req engine.FindRequest) ([]store.Comment, error) {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("mockError")
|
||||||
|
},
|
||||||
|
}
|
||||||
site1Req := engine.FindRequest{Locator: store.Locator{SiteID: "site1", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)}
|
site1Req := engine.FindRequest{Locator: store.Locator{SiteID: "site1", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)}
|
||||||
site2Req := engine.FindRequest{Locator: store.Locator{SiteID: "site2", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)}
|
site2Req := engine.FindRequest{Locator: store.Locator{SiteID: "site2", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)}
|
||||||
engineMock.On("Find", site1Req).Return(nil, nil)
|
|
||||||
engineMock.On("Find", site2Req).Return(nil, fmt.Errorf("mockError"))
|
|
||||||
b := DataStore{Engine: &engineMock, EditDuration: 10 * time.Millisecond, ImageService: imgSvc}
|
b := DataStore{Engine: &engineMock, EditDuration: 10 * time.Millisecond, ImageService: imgSvc}
|
||||||
|
|
||||||
// One call without error and one with error
|
// One call without error and one with error
|
||||||
mockStore.On("Info").Once().Return(image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil)
|
|
||||||
err := b.ResubmitStagingImages([]string{"site1", "site2"})
|
err := b.ResubmitStagingImages([]string{"site1", "site2"})
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "problem finding comments for site site2: mockError")
|
assert.Contains(t, err.Error(), "problem finding comments for site site2: mockError")
|
||||||
|
|
||||||
mockStore.AssertNumberOfCalls(t, "Info", 1)
|
assert.Equal(t, 1, len(mockStore.InfoCalls()))
|
||||||
|
assert.Equal(t, 2, len(engineMock.FindCalls()))
|
||||||
|
assert.Equal(t, site1Req, engineMock.FindCalls()[0].Req)
|
||||||
|
assert.Equal(t, site2Req, engineMock.FindCalls()[1].Req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_alterComment(t *testing.T) {
|
func TestService_alterComment(t *testing.T) {
|
||||||
engineMock := engine.MockInterface{}
|
engineMock := engine.InterfaceMock{
|
||||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}).Return(false, nil)
|
FlagFunc: func(req engine.FlagRequest) (bool, error) {
|
||||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}).Return(false, nil)
|
return false, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
svc := DataStore{Engine: &engineMock}
|
svc := DataStore{Engine: &engineMock}
|
||||||
|
|
||||||
r := svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"},
|
r := svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"},
|
||||||
@@ -1514,24 +1574,48 @@ func TestService_alterComment(t *testing.T) {
|
|||||||
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"}},
|
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"}},
|
||||||
store.User{Name: "dev", ID: "devid", Admin: true})
|
store.User{Name: "dev", ID: "devid", Admin: true})
|
||||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"}}, r, "ip not cleaned")
|
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid"}}, r, "ip not cleaned")
|
||||||
|
assert.Equal(t, 4, len(engineMock.FlagCalls()))
|
||||||
|
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[0].Req)
|
||||||
|
assert.Equal(t, engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}, engineMock.FlagCalls()[1].Req)
|
||||||
|
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[2].Req)
|
||||||
|
assert.Equal(t, engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}, engineMock.FlagCalls()[3].Req)
|
||||||
|
|
||||||
engineMock = engine.MockInterface{}
|
first := true
|
||||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}).Return(false, nil)
|
engineMock = engine.InterfaceMock{
|
||||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}).Return(true, nil)
|
FlagFunc: func(req engine.FlagRequest) (bool, error) {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
svc = DataStore{Engine: &engineMock}
|
svc = DataStore{Engine: &engineMock}
|
||||||
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid", Verified: true}},
|
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid", Verified: true}},
|
||||||
store.User{Name: "dev", ID: "devid", Admin: false})
|
store.User{Name: "dev", ID: "devid", Admin: false})
|
||||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", ID: "devid", Verified: true}}, r, "verified set")
|
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", ID: "devid", Verified: true}}, r, "verified set")
|
||||||
|
assert.Equal(t, 2, len(engineMock.FlagCalls()))
|
||||||
|
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[0].Req)
|
||||||
|
assert.Equal(t, engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}, engineMock.FlagCalls()[1].Req)
|
||||||
|
|
||||||
engineMock = engine.MockInterface{}
|
first = true
|
||||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}).Return(true, nil)
|
engineMock = engine.InterfaceMock{
|
||||||
engineMock.On("Flag", engine.FlagRequest{Flag: engine.Verified, UserID: "devid"}).Return(false, nil)
|
FlagFunc: func(req engine.FlagRequest) (bool, error) {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
svc = DataStore{Engine: &engineMock}
|
svc = DataStore{Engine: &engineMock}
|
||||||
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid", Verified: true},
|
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", ID: "devid", Verified: true},
|
||||||
Locator: store.Locator{URL: "javascript:alert('XSS1')"}},
|
Locator: store.Locator{URL: "javascript:alert('XSS1')"}},
|
||||||
store.User{Name: "dev", ID: "devid", Admin: false})
|
store.User{Name: "dev", ID: "devid", Admin: false})
|
||||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", Verified: true, Blocked: true, ID: "devid"},
|
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", Verified: true, Blocked: true, ID: "devid"},
|
||||||
Deleted: false}, r, "blocked")
|
Deleted: false}, r, "blocked")
|
||||||
|
assert.Equal(t, 1, len(engineMock.FlagCalls()))
|
||||||
|
assert.Equal(t, engine.FlagRequest{Flag: engine.Blocked, UserID: "devid"}, engineMock.FlagCalls()[0].Req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Benchmark_ServiceCreate(b *testing.B) {
|
func Benchmark_ServiceCreate(b *testing.B) {
|
||||||
@@ -1556,9 +1640,33 @@ func Benchmark_ServiceCreate(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestService_DoubleClose_Bolt(t *testing.T) {
|
||||||
|
dbFile := fmt.Sprintf("%s/test-remark42-%d.db", os.TempDir(), rand.Intn(9999999999))
|
||||||
|
defer func() { _ = os.Remove(dbFile) }()
|
||||||
|
|
||||||
|
boltStore, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: dbFile, SiteID: "radio-t"})
|
||||||
|
svc := DataStore{Engine: boltStore, EditDuration: 50 * time.Millisecond, AdminStore: admin.NewStaticKeyStore("secret 123")}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NoError(t, boltStore.Close())
|
||||||
|
assert.NoError(t, boltStore.Close(), "second call should not result in panic or errors")
|
||||||
|
assert.NoError(t, svc.Close())
|
||||||
|
assert.NoError(t, svc.Close(), "second call should not result in panic or errors")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_DoubleClose_Static(t *testing.T) {
|
||||||
|
ks := admin.NewStaticKeyStore("secret 123")
|
||||||
|
eng, teardown := prepStoreEngine(t)
|
||||||
|
defer teardown()
|
||||||
|
b := DataStore{Engine: eng, AdminStore: ks,
|
||||||
|
TitleExtractor: NewTitleExtractor(http.Client{Timeout: 5 * time.Second})}
|
||||||
|
b.Close()
|
||||||
|
// second call should not result in panic or errors
|
||||||
|
b.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// makes new boltdb, put two records
|
// makes new boltdb, put two records
|
||||||
func prepStoreEngine(t *testing.T) (e engine.Interface, teardown func()) {
|
func prepStoreEngine(t *testing.T) (e engine.Interface, teardown func()) {
|
||||||
testDBLoc, err := ioutil.TempDir("", "test_image_r42")
|
testDBLoc, err := os.MkdirTemp("", "test_image_r42")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testDB := path.Join(testDBLoc, "test.db")
|
testDB := path.Join(testDBLoc, "test.db")
|
||||||
_ = os.Remove(testDB)
|
_ = os.Remove(testDB)
|
||||||
|
|||||||
@@ -123,3 +123,10 @@ func TestTitle_GetFailed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Equal(t, int32(1), atomic.LoadInt32(&hits), "hit once, errors cached")
|
assert.Equal(t, int32(1), atomic.LoadInt32(&hits), "hit once, errors cached")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTitle_DoubleClosed(*testing.T) {
|
||||||
|
ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second})
|
||||||
|
ex.Close()
|
||||||
|
// second call should not result in panic
|
||||||
|
ex.Close()
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
func TestMakeTree(t *testing.T) {
|
func TestMakeTree(t *testing.T) {
|
||||||
loc := store.Locator{URL: "url", SiteID: "site"}
|
loc := store.Locator{URL: "url", SiteID: "site"}
|
||||||
ts := func(min int, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
|
ts := func(min, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
|
||||||
|
|
||||||
// unsorted by purpose
|
// unsorted by purpose
|
||||||
comments := []store.Comment{
|
comments := []store.Comment{
|
||||||
@@ -54,7 +54,7 @@ func TestMakeTree(t *testing.T) {
|
|||||||
|
|
||||||
func TestMakeEmptySubtree(t *testing.T) {
|
func TestMakeEmptySubtree(t *testing.T) {
|
||||||
loc := store.Locator{URL: "url", SiteID: "site"}
|
loc := store.Locator{URL: "url", SiteID: "site"}
|
||||||
ts := func(min int, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
|
ts := func(min, sec int) time.Time { return time.Date(2017, 12, 25, 19, min, sec, 0, time.UTC) }
|
||||||
|
|
||||||
// unsorted by purpose
|
// unsorted by purpose
|
||||||
comments := []store.Comment{
|
comments := []store.Comment{
|
||||||
|
|||||||
@@ -1,38 +1,24 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"embed"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
log "github.com/go-pkgz/lgr"
|
|
||||||
"github.com/rakyll/statik/fs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FS stores link to statikFS if it exists
|
//go:embed static
|
||||||
type FS struct {
|
var templateFS embed.FS
|
||||||
statik http.FileSystem
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileReader describes methods of filesystem
|
// Read reads either template from disk if it exists, or from embedded template
|
||||||
type FileReader interface {
|
func Read(path string) ([]byte, error) {
|
||||||
ReadFile(path string) ([]byte, error)
|
if _, err := os.Stat(filepath.Clean(path)); err == nil {
|
||||||
}
|
return os.ReadFile(filepath.Clean(path))
|
||||||
|
|
||||||
// NewFS returns new FS instance, which will read from statik if it's available and from fs otherwise
|
|
||||||
func NewFS() *FS {
|
|
||||||
f := &FS{}
|
|
||||||
if statikFS, err := fs.NewWithNamespace("templates"); err == nil {
|
|
||||||
log.Printf("[INFO] templates will be read from statik")
|
|
||||||
f.statik = statikFS
|
|
||||||
}
|
}
|
||||||
return f
|
// remove "static/" prefix from path
|
||||||
}
|
var contentFS, err = fs.Sub(templateFS, "static")
|
||||||
|
if err != nil {
|
||||||
// ReadFile depends on statik achieve exists
|
return nil, err
|
||||||
func (f *FS) ReadFile(path string) ([]byte, error) {
|
|
||||||
if f.statik != nil {
|
|
||||||
return fs.ReadFile(f.statik, filepath.Join("/", path)) //nolint:gocritic // root folder is a requirement for statik
|
|
||||||
}
|
}
|
||||||
return os.ReadFile(filepath.Clean(path))
|
return fs.ReadFile(contentFS, filepath.Clean(path))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,12 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewFS(t *testing.T) {
|
func Test_Read(t *testing.T) {
|
||||||
fs := NewFS()
|
file, err := Read("testdata/template.html.tmpl")
|
||||||
assert.NotNil(t, &fs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFS_ReadFile(t *testing.T) {
|
|
||||||
fs := NewFS()
|
|
||||||
|
|
||||||
file, err := fs.ReadFile("testdata/template.html.tmpl")
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []byte("template\n"), file)
|
assert.Equal(t, []byte("template\n"), file)
|
||||||
|
|
||||||
file, err = fs.ReadFile("testdata/bad_path.html.tmpl")
|
file, err = Read("testdata/bad_path.html.tmpl")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Nil(t, file)
|
assert.Nil(t, file)
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-50
@@ -1,79 +1,78 @@
|
|||||||
module github.com/umputun/remark42/backend
|
module github.com/umputun/remark42/backend
|
||||||
|
|
||||||
go 1.17
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Depado/bfchroma v1.3.0
|
github.com/Depado/bfchroma/v2 v2.0.0
|
||||||
github.com/PuerkitoBio/goquery v1.8.0
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/alecthomas/chroma v0.10.0
|
github.com/alecthomas/chroma/v2 v2.8.0
|
||||||
github.com/didip/tollbooth/v6 v6.1.2
|
github.com/didip/tollbooth/v7 v7.0.1
|
||||||
github.com/didip/tollbooth_chi v0.0.0-20220429013743-da966f2f674b
|
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f
|
||||||
github.com/go-chi/chi/v5 v5.0.7
|
github.com/go-chi/chi/v5 v5.0.10
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/go-chi/render v1.0.1
|
github.com/go-chi/render v1.0.3
|
||||||
github.com/go-pkgz/auth v1.19.1-0.20220605174438-06e72788bcbb
|
github.com/go-pkgz/auth v1.21.0
|
||||||
github.com/go-pkgz/jrpc v0.2.0
|
github.com/go-pkgz/jrpc v0.3.0
|
||||||
github.com/go-pkgz/lcw v0.8.1
|
github.com/go-pkgz/lcw v1.0.2
|
||||||
github.com/go-pkgz/lgr v0.10.4
|
github.com/go-pkgz/lgr v0.11.0
|
||||||
github.com/go-pkgz/notify v0.1.1
|
github.com/go-pkgz/notify v0.2.0
|
||||||
github.com/go-pkgz/repeater v1.1.3
|
github.com/go-pkgz/repeater v1.1.3
|
||||||
github.com/go-pkgz/rest v1.15.0
|
github.com/go-pkgz/rest v1.17.0
|
||||||
github.com/go-pkgz/syncs v1.2.0
|
github.com/go-pkgz/syncs v1.3.2
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/feeds v1.1.1
|
github.com/gorilla/feeds v1.1.1
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/jessevdk/go-flags v1.5.0
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
github.com/kyokomi/emoji/v2 v2.2.9
|
github.com/kyokomi/emoji/v2 v2.2.12
|
||||||
github.com/microcosm-cc/bluemonday v1.0.18
|
github.com/microcosm-cc/bluemonday v1.0.25
|
||||||
github.com/rakyll/statik v0.1.7
|
github.com/rs/xid v1.5.0
|
||||||
github.com/rs/xid v1.4.0
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
github.com/russross/blackfriday/v2 v2.1.0
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.8.2
|
||||||
go.etcd.io/bbolt v1.3.6
|
go.etcd.io/bbolt v1.3.7
|
||||||
go.uber.org/goleak v1.1.12
|
go.uber.org/goleak v1.2.1
|
||||||
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898
|
golang.org/x/crypto v0.12.0
|
||||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9
|
golang.org/x/image v0.11.0
|
||||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
|
golang.org/x/net v0.14.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute v1.6.1 // indirect
|
cloud.google.com/go/compute v1.23.0 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dghubble/oauth1 v0.7.1 // indirect
|
github.com/dghubble/oauth1 v0.7.2 // indirect
|
||||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/go-chi/chi v4.1.1+incompatible // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/go-oauth2/oauth2/v4 v4.4.3 // indirect
|
github.com/go-oauth2/oauth2/v4 v4.5.2 // indirect
|
||||||
github.com/go-pkgz/email v0.3.0 // indirect
|
github.com/go-pkgz/email v0.4.1 // indirect
|
||||||
github.com/go-pkgz/expirable-cache v0.0.3 // indirect
|
github.com/go-pkgz/expirable-cache v1.0.0 // indirect
|
||||||
github.com/go-redis/redis/v7 v7.4.1 // indirect
|
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||||
github.com/go-stack/stack v1.8.1 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/gorilla/css v1.0.0 // indirect
|
github.com/gorilla/css v1.0.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||||
github.com/klauspost/compress v1.15.2 // indirect
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 // indirect
|
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/slack-go/slack v0.10.3 // indirect
|
github.com/slack-go/slack v0.12.2 // indirect
|
||||||
github.com/stretchr/objx v0.3.0 // indirect
|
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.1 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.3 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||||
go.mongodb.org/mongo-driver v1.9.1 // indirect
|
go.mongodb.org/mongo-driver v1.12.1 // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
|
golang.org/x/oauth2 v0.11.0 // indirect
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
golang.org/x/sys v0.11.0 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.12.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
|
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/protobuf v1.28.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
+156
-656
File diff suppressed because it is too large
Load Diff
+17
-2
@@ -119,12 +119,27 @@ GET {{host}}/api/v1/rss/reply?site={{site}}&user={{user}}
|
|||||||
GET {{host}}/api/v1/avatar/blah
|
GET {{host}}/api/v1/avatar/blah
|
||||||
|
|
||||||
### send confirmation token for current user to specified email. auth token for dev user for secret=12345.
|
### send confirmation token for current user to specified email. auth token for dev user for secret=12345.
|
||||||
POST {{host}}/api/v1/email/subscribe?site={{site}}&address={{email}}
|
### in case the user logged in with the same email, it will be confirmed right away with "updated" set to "true" in the response,
|
||||||
|
### and no email will be sent.
|
||||||
|
POST {{host}}/api/v1/email/subscribe
|
||||||
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
|
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"site": "{{site}}",
|
||||||
|
"address": "{{email}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
### add email for notifications for current user via token from email. auth token for dev user for secret=12345.
|
### add email for notifications for current user via token from email. auth token for dev user for secret=12345.
|
||||||
POST {{host}}/api/v1/email/confirm?site={{site}}&tkn={{token}}
|
POST {{host}}/api/v1/email/confirm
|
||||||
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
|
X-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcmsiLCJleHAiOjE5NzYwNTY3NTYsImp0aSI6IjJlOGJmMTE5OTI0MjQxMDRjYjFhZGRlODllMWYwNGFiMTg4YWZjMzQiLCJpYXQiOjE1NzYwNTY0NTYsImlzcyI6InJlbWFyazQyIiwidXNlciI6eyJuYW1lIjoiZGV2X3VzZXIiLCJpZCI6ImRldl91c2VyIiwicGljdHVyZSI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MC9hcGkvdjEvYXZhdGFyL2NjZmEyYWJkMDE2Njc2MDViNGUxZmM0ZmNiOTFiMWUxYWYzMjMyNDAuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.6Qt5s2enBMRC-Jmsua01yViVYI95Dx6BPBMaNjj36d4
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"site": "{{site}}",
|
||||||
|
"token": "{{token}}"
|
||||||
|
}
|
||||||
|
|
||||||
### get current user email. auth token for dev user for secret=12345.
|
### get current user email. auth token for dev user for secret=12345.
|
||||||
GET {{host}}/api/v1/email?site={{site}}
|
GET {{host}}/api/v1/email?site={{site}}
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright 2022 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
// Version is the current tagged release of the library.
|
||||||
|
const Version = "1.23.0"
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Changes
|
||||||
|
|
||||||
|
## [0.2.3](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.2...compute/metadata/v0.2.3) (2022-12-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **compute/metadata:** Switch DNS lookup to an absolute lookup ([119b410](https://github.com/googleapis/google-cloud-go/commit/119b41060c7895e45e48aee5621ad35607c4d021)), refs [#7165](https://github.com/googleapis/google-cloud-go/issues/7165)
|
||||||
|
|
||||||
|
## [0.2.2](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.2.1...compute/metadata/v0.2.2) (2022-12-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **compute/metadata:** Set IdleConnTimeout for http.Client ([#7084](https://github.com/googleapis/google-cloud-go/issues/7084)) ([766516a](https://github.com/googleapis/google-cloud-go/commit/766516aaf3816bfb3159efeea65aa3d1d205a3e2)), refs [#5430](https://github.com/googleapis/google-cloud-go/issues/5430)
|
||||||
|
|
||||||
|
## [0.1.0] (2022-10-26)
|
||||||
|
|
||||||
|
Initial release of metadata being it's own module.
|
||||||
Generated
Vendored
+1
-1
@@ -187,7 +187,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2014 Google Inc.
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Compute API
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/cloud.google.com/go/compute/metadata)
|
||||||
|
|
||||||
|
This is a utility library for communicating with Google Cloud metadata service
|
||||||
|
on Google Cloud.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get cloud.google.com/go/compute/metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
## Go Version Support
|
||||||
|
|
||||||
|
See the [Go Versions Supported](https://github.com/googleapis/google-cloud-go#go-versions-supported)
|
||||||
|
section in the root directory's README.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome. Please, see the [CONTRIBUTING](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md)
|
||||||
|
document for details.
|
||||||
|
|
||||||
|
Please note that this project is released with a Contributor Code of Conduct.
|
||||||
|
By participating in this project you agree to abide by its terms. See
|
||||||
|
[Contributor Code of Conduct](https://github.com/GoogleCloudPlatform/google-cloud-go/blob/main/CONTRIBUTING.md#contributor-code-of-conduct)
|
||||||
|
for more information.
|
||||||
+4
-2
@@ -16,7 +16,7 @@
|
|||||||
// metadata and API service accounts.
|
// metadata and API service accounts.
|
||||||
//
|
//
|
||||||
// This package is a wrapper around the GCE metadata service,
|
// This package is a wrapper around the GCE metadata service,
|
||||||
// as documented at https://developers.google.com/compute/docs/metadata.
|
// as documented at https://cloud.google.com/compute/docs/metadata/overview.
|
||||||
package metadata // import "cloud.google.com/go/compute/metadata"
|
package metadata // import "cloud.google.com/go/compute/metadata"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -70,7 +70,9 @@ func newDefaultHTTPClient() *http.Client {
|
|||||||
Timeout: 2 * time.Second,
|
Timeout: 2 * time.Second,
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
}).Dial,
|
}).Dial,
|
||||||
|
IdleConnTimeout: 60 * time.Second,
|
||||||
},
|
},
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +147,7 @@ func testOnGCE() bool {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
resolver := &net.Resolver{}
|
resolver := &net.Resolver{}
|
||||||
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal")
|
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal.")
|
||||||
if err != nil || len(addrs) == 0 {
|
if err != nil || len(addrs) == 0 {
|
||||||
resc <- false
|
resc <- false
|
||||||
return
|
return
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2022 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// This file, and the {{.RootMod}} import, won't actually become part of
|
||||||
|
// the resultant binary.
|
||||||
|
//go:build modhack
|
||||||
|
// +build modhack
|
||||||
|
|
||||||
|
package metadata
|
||||||
|
|
||||||
|
// Necessary for safely adding multi-module repo. See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository
|
||||||
|
import _ "cloud.google.com/go/compute/internal"
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
run:
|
|
||||||
timeout: 5m
|
|
||||||
|
|
||||||
linters:
|
|
||||||
enable:
|
|
||||||
- megacheck
|
|
||||||
- govet
|
|
||||||
disable:
|
|
||||||
- errcheck
|
|
||||||
- maligned
|
|
||||||
- prealloc
|
|
||||||
disable-all: false
|
|
||||||
presets:
|
|
||||||
- bugs
|
|
||||||
- unused
|
|
||||||
fast: false
|
|
||||||
Generated
Vendored
+10
-9
@@ -5,7 +5,7 @@ type: docker
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test
|
- name: test
|
||||||
image: golang:1.13.1
|
image: golang:latest
|
||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
path: /go
|
path: /go
|
||||||
@@ -13,13 +13,14 @@ steps:
|
|||||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||||
|
|
||||||
- name: linter
|
- name: linter
|
||||||
image: golang:1.13.1
|
image: golang:latest
|
||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
path: /go
|
path: /go
|
||||||
commands:
|
commands:
|
||||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.20.0
|
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s
|
||||||
- ./bin/golangci-lint run
|
- ./bin/golangci-lint run --timeout 5m
|
||||||
|
- rm -r ./bin/
|
||||||
|
|
||||||
- name: coverage
|
- name: coverage
|
||||||
image: plugins/codecov
|
image: plugins/codecov
|
||||||
@@ -37,8 +38,8 @@ steps:
|
|||||||
token:
|
token:
|
||||||
from_secret: telegram_token
|
from_secret: telegram_token
|
||||||
message: >
|
message: >
|
||||||
*{{repo.name}}*
|
*{{repo.name}}*
|
||||||
[Build {{build.number}}]({{build.link}}) by {{commit.author}} {{#success build.status}}succeeded{{else}}failed{{/success}} in {{buildtime build.started}}
|
[Build {{build.number}}]({{build.link}}) by {{commit.author}} {{#success build.status}}succeeded{{else}}failed{{/success}} in {{buildtime build.started}}
|
||||||
`{{truncate commit.sha 8}}`: "{{commit.message}}"
|
`{{truncate commit.sha 8}}`: "{{commit.message}}"
|
||||||
when:
|
when:
|
||||||
status:
|
status:
|
||||||
@@ -48,4 +49,4 @@ steps:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
host:
|
host:
|
||||||
path: /var/lib/cache/godeps/
|
path: /var/lib/cache/godeps/
|
||||||
Generated
Vendored
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user