Ausgangspunkt: Instagram hält Story-Insights nach eigener Aussage nur
24 Stunden vor, auch der offizielle Datenexport enthält sie nicht mehr
danach. Der Standbild-Screenshot beim Prüfen entsteht direkt beim
Veröffentlichen, bevor nennenswerte Kennzahlen existieren — er kann
das strukturell nicht auffangen. Eine OAuth-Anbindung allein löst das
auch nicht: selbst mit API-Zugriff bräuchte es einen Abruf innerhalb
desselben 24h-Fensters.
- Migration 0007: asset.purpose ('initial' | 'insights', Default
'initial' erhält die Bedeutung aller Bestandszeilen). Ein Beitrag
kann jetzt mehrere Insights-Nachweise über die Zeit bekommen.
GetLatestAssetForSubmission berücksichtigt weiterhin nur 'initial',
damit ein späterer Insights-Upload nie den beim Archivieren
referenzierten Original-Screenshot verdrängt.
- internal/web/insights_reminder.go: computeInsightsReminder — reine,
ungetestete gegen echte Instagram-Daten, aber isoliert testbare
Logik fürs Erinnerungs-Timing (Produktentscheidung, keine Rechtsnorm,
daher nicht in rules/*.yaml).
- GET /beitraege/{id} zeigt die Erinnerung bei veröffentlichten
"story"-Beiträgen ohne existierendes insights-Asset; POST
/beitraege/{id}/insights speichert einen weiteren Screenshot (gleiche
Validierung wie das initiale Standbild, wiederverwendet über
readUploadedAsset/storeAsset mit purpose-Parameter).
- Bewusst nur In-App-Banner in dieser Ausbaustufe, kein Mail-/Push-
Versand — dafür fehlt aktuell ein SMTP-Relay/Versanddienst, siehe
CLAUDE.md-Hinweis dazu.
Volle Testsuite inkl. echter Postgres-Tests grün; End-to-End gegen
einen laufenden Server verifiziert (Story archivieren → Erinnerung
sichtbar → Insights-Upload → Erinnerung verschwindet, Nachweis
gelistet, Mandantentrennung beim Upload durchgesetzt).
153 lines
4.7 KiB
Go
153 lines
4.7 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/netcell-it/deklarix/internal/store"
|
|
)
|
|
|
|
func TestAssetCreateAndGetLatest(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
sub, err := s.CreateSubmission(ctx, accID, "instagram", "feed", "...")
|
|
if err != nil {
|
|
t.Fatalf("CreateSubmission: %v", err)
|
|
}
|
|
|
|
a, err := s.CreateAsset(ctx, sub.ID, "image", "initial", "/var/lib/deklarix/assets/abc.jpg", "deadbeef")
|
|
if err != nil {
|
|
t.Fatalf("CreateAsset: %v", err)
|
|
}
|
|
if a.SubmissionID != sub.ID || a.Kind != "image" || a.Purpose != "initial" {
|
|
t.Fatalf("CreateAsset = %+v, unerwartete Werte", a)
|
|
}
|
|
|
|
got, err := s.GetLatestAssetForSubmission(ctx, sub.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestAssetForSubmission: %v", err)
|
|
}
|
|
if got.ID != a.ID || got.SHA256 != "deadbeef" {
|
|
t.Fatalf("GetLatestAssetForSubmission = %+v, want %+v", got, a)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestAssetForSubmissionNotFoundWhenNoneUploaded(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
sub, err := s.CreateSubmission(ctx, accID, "instagram", "feed", "...")
|
|
if err != nil {
|
|
t.Fatalf("CreateSubmission: %v", err)
|
|
}
|
|
|
|
_, err = s.GetLatestAssetForSubmission(ctx, sub.ID)
|
|
if !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("err = %v, want store.ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestAssetForSubmissionReturnsNewestWhenMultiple(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
sub, err := s.CreateSubmission(ctx, accID, "instagram", "feed", "...")
|
|
if err != nil {
|
|
t.Fatalf("CreateSubmission: %v", err)
|
|
}
|
|
|
|
if _, err := s.CreateAsset(ctx, sub.ID, "image", "initial", "/tmp/erstes.jpg", "erstehash"); err != nil {
|
|
t.Fatalf("CreateAsset (1): %v", err)
|
|
}
|
|
second, err := s.CreateAsset(ctx, sub.ID, "image", "initial", "/tmp/zweites.jpg", "zweitehash")
|
|
if err != nil {
|
|
t.Fatalf("CreateAsset (2): %v", err)
|
|
}
|
|
|
|
got, err := s.GetLatestAssetForSubmission(ctx, sub.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestAssetForSubmission: %v", err)
|
|
}
|
|
if got.ID != second.ID {
|
|
t.Fatalf("expected the newest asset, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestGetLatestAssetForSubmissionIgnoresInsightsAssets(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
sub, err := s.CreateSubmission(ctx, accID, "instagram", "story", "...")
|
|
if err != nil {
|
|
t.Fatalf("CreateSubmission: %v", err)
|
|
}
|
|
|
|
initial, err := s.CreateAsset(ctx, sub.ID, "image", "initial", "/tmp/initial.jpg", "initialhash")
|
|
if err != nil {
|
|
t.Fatalf("CreateAsset (initial): %v", err)
|
|
}
|
|
if _, err := s.CreateAsset(ctx, sub.ID, "image", "insights", "/tmp/insights.jpg", "insightshash"); err != nil {
|
|
t.Fatalf("CreateAsset (insights): %v", err)
|
|
}
|
|
|
|
got, err := s.GetLatestAssetForSubmission(ctx, sub.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestAssetForSubmission: %v", err)
|
|
}
|
|
if got.ID != initial.ID {
|
|
t.Fatalf("expected GetLatestAssetForSubmission to keep returning the initial asset even after an insights asset was added later, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestListAssetsForSubmissionReturnsAllPurposes(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
sub, err := s.CreateSubmission(ctx, accID, "instagram", "story", "...")
|
|
if err != nil {
|
|
t.Fatalf("CreateSubmission: %v", err)
|
|
}
|
|
if _, err := s.CreateAsset(ctx, sub.ID, "image", "initial", "/tmp/initial.jpg", "h1"); err != nil {
|
|
t.Fatalf("CreateAsset (initial): %v", err)
|
|
}
|
|
if _, err := s.CreateAsset(ctx, sub.ID, "image", "insights", "/tmp/insights.jpg", "h2"); err != nil {
|
|
t.Fatalf("CreateAsset (insights): %v", err)
|
|
}
|
|
|
|
list, err := s.ListAssetsForSubmission(ctx, sub.ID)
|
|
if err != nil {
|
|
t.Fatalf("ListAssetsForSubmission: %v", err)
|
|
}
|
|
if len(list) != 2 {
|
|
t.Fatalf("expected 2 assets, got %d: %+v", len(list), list)
|
|
}
|
|
if list[0].Purpose != "initial" || list[1].Purpose != "insights" {
|
|
t.Fatalf("expected initial before insights (created_at order), got %+v", list)
|
|
}
|
|
}
|
|
|
|
func TestAssetIsAppendOnly(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
sub, err := s.CreateSubmission(ctx, accID, "instagram", "feed", "...")
|
|
if err != nil {
|
|
t.Fatalf("CreateSubmission: %v", err)
|
|
}
|
|
a, err := s.CreateAsset(ctx, sub.ID, "image", "initial", "/tmp/x.jpg", "hash")
|
|
if err != nil {
|
|
t.Fatalf("CreateAsset: %v", err)
|
|
}
|
|
|
|
_, err = s.Pool.Exec(ctx, `UPDATE asset SET sha256 = 'geaendert' WHERE id = $1`, a.ID)
|
|
if err == nil {
|
|
t.Fatal("expected UPDATE on asset to be rejected by the append-only trigger")
|
|
}
|
|
_, err = s.Pool.Exec(ctx, `DELETE FROM asset WHERE id = $1`, a.ID)
|
|
if err == nil {
|
|
t.Fatal("expected DELETE on asset to be rejected by the append-only trigger")
|
|
}
|
|
}
|